diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 489a520f2..c3fb0f722 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,8 +1,18 @@
#### Features ๐
+- `animated` keyword implemented for connections. [#652](https://github.com/terrastruct/d2/pull/652)
+
#### Improvements ๐งน
- ELK layouts tuned to have better defaults. [#627](https://github.com/terrastruct/d2/pull/627)
- Code snippets of unrecognized languages will render (just without syntax highlighting). [#650](https://github.com/terrastruct/d2/pull/650)
+- Adds sketched versions of arrowheads. [#656](https://github.com/terrastruct/d2/pull/656)
#### Bugfixes โ๏ธ
+
+- Fixes arrowheads sometimes appearing broken in dagre layouts. [#649](https://github.com/terrastruct/d2/pull/649)
+- Fixes attributes being ignored for `sql_table` to `sql_table` connections. [#658](https://github.com/terrastruct/d2/pull/658)
+- Fixes tooltip/link attributes being ignored for `sql_table` and `class`. [#658](https://github.com/terrastruct/d2/pull/658)
+- Fixes arrowheads sometimes appearing broken with sketch on. [#656](https://github.com/terrastruct/d2/pull/656)
+- Fixes code snippets not being tall enough with leading newlines. [#664](https://github.com/terrastruct/d2/pull/664)
+- Icon URLs that needed escaping (e.g. with ampersands) are handled correctly by CLI. [#666](https://github.com/terrastruct/d2/pull/666)
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 48498a767..c89a4853e 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -757,13 +757,15 @@ func flattenContainer(g *d2graph.Graph, obj *d2graph.Object) {
// TODO more attributes
if e.SrcTableColumnIndex != nil {
newEdge.SrcTableColumnIndex = new(int)
+ newEdge.SrcArrowhead = e.SrcArrowhead
*newEdge.SrcTableColumnIndex = *e.SrcTableColumnIndex
}
if e.DstTableColumnIndex != nil {
newEdge.DstTableColumnIndex = new(int)
+ newEdge.DstArrowhead = e.DstArrowhead
*newEdge.DstTableColumnIndex = *e.DstTableColumnIndex
}
- newEdge.Attributes.Label = e.Attributes.Label
+ newEdge.Attributes = e.Attributes
newEdge.References = e.References
}
updatedEdges := []*d2graph.Edge{}
diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index 81db413f2..a432f078e 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -1597,6 +1597,25 @@ b`, g.Objects[0].Attributes.Label.Value)
}
},
},
+ {
+ name: "table_connection_attr",
+
+ text: `x: {
+ shape: sql_table
+ y
+}
+a: {
+ shape: sql_table
+ b
+}
+x.y -> a.b: {
+ style.animated: true
+}
+`,
+ assertions: func(t *testing.T, g *d2graph.Graph) {
+ tassert.Equal(t, "true", g.Edges[0].Attributes.Style.Animated.Value)
+ },
+ },
{
name: "class_paren",
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 1dd1791a6..85521f0ac 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -1027,6 +1027,27 @@ func GetTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
var h int
if t.Language != "" {
w, h = ruler.Measure(d2fonts.SourceCodePro.Font(t.FontSize, d2fonts.FONT_STYLE_REGULAR), t.Text)
+
+ // count empty leading and trailing lines since ruler will not be able to measure it
+ lines := strings.Split(t.Text, "\n")
+ leadingLines := 0
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ leadingLines++
+ } else {
+ break
+ }
+ }
+ trailingLines := 0
+ for i := len(lines) - 1; i >= 0; i-- {
+ if strings.TrimSpace(lines[i]) == "" {
+ trailingLines++
+ } else {
+ break
+ }
+ }
+ h += t.FontSize * (leadingLines + trailingLines)
+
// padding
w += 12
h += 12
diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go
index 0d5c5d569..7ac2a44e4 100644
--- a/d2layouts/d2dagrelayout/layout.go
+++ b/d2layouts/d2dagrelayout/layout.go
@@ -30,6 +30,8 @@ var setupJS string
//go:embed dagre.js
var dagreJS string
+const MIN_SEGMENT_LEN = 10
+
type ConfigurableOpts struct {
NodeSep int `json:"nodesep"`
EdgeSep int `json:"edgesep"`
@@ -247,6 +249,47 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
+ // arrowheads can appear broken if segments are very short from dagre routing a point just outside the shape
+ // to fix this, we try extending the previous segment into the shape instead of having a very short segment
+ if !start.Equals(points[0]) && startIndex+2 < len(points) {
+ newStartingSegment := *geo.NewSegment(start, points[startIndex+1])
+ if newStartingSegment.Length() < MIN_SEGMENT_LEN {
+ // we don't want a very short segment right next to the source because it will mess up the arrowhead
+ // instead we want to extend the next segment into the shape border if possible
+ nextStart := points[startIndex+1]
+ nextEnd := points[startIndex+2]
+
+ // Note: in other direction to extend towards source
+ nextSegment := *geo.NewSegment(nextStart, nextEnd)
+ v := nextSegment.ToVector()
+ extendedStart := nextEnd.ToVector().Add(v.AddLength(MIN_SEGMENT_LEN)).ToPoint()
+ extended := *geo.NewSegment(nextEnd, extendedStart)
+
+ if intersections := edge.Src.Box.Intersections(extended); len(intersections) > 0 {
+ start = intersections[0]
+ startIndex += 1
+ }
+ }
+ }
+ if !end.Equals(points[len(points)-1]) && endIndex-2 >= 0 {
+ newEndingSegment := *geo.NewSegment(end, points[endIndex-1])
+ if newEndingSegment.Length() < MIN_SEGMENT_LEN {
+ // extend the prev segment into the shape border if possible
+ prevStart := points[endIndex-2]
+ prevEnd := points[endIndex-1]
+
+ prevSegment := *geo.NewSegment(prevStart, prevEnd)
+ v := prevSegment.ToVector()
+ extendedEnd := prevStart.ToVector().Add(v.AddLength(MIN_SEGMENT_LEN)).ToPoint()
+ extended := *geo.NewSegment(prevStart, extendedEnd)
+
+ if intersections := edge.Dst.Box.Intersections(extended); len(intersections) > 0 {
+ end = intersections[0]
+ endIndex -= 1
+ }
+ }
+ }
+
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box)
@@ -263,18 +306,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
path := make([]*geo.Point, 0)
path = append(path, points[0])
- path = append(path, points[0].AddVector(vectors[0].Multiply(.8)))
- for i := 1; i < len(vectors)-2; i++ {
- p := points[i]
- v := vectors[i]
- path = append(path, p.AddVector(v.Multiply(.2)))
- path = append(path, p.AddVector(v.Multiply(.5)))
- path = append(path, p.AddVector(v.Multiply(.8)))
+ if len(vectors) > 1 {
+ path = append(path, points[0].AddVector(vectors[0].Multiply(.8)))
+ for i := 1; i < len(vectors)-2; i++ {
+ p := points[i]
+ v := vectors[i]
+ path = append(path, p.AddVector(v.Multiply(.2)))
+ path = append(path, p.AddVector(v.Multiply(.5)))
+ path = append(path, p.AddVector(v.Multiply(.8)))
+ }
+ path = append(path, points[len(points)-2].AddVector(vectors[len(vectors)-1].Multiply(.2)))
+ edge.IsCurve = true
}
- path = append(path, points[len(points)-2].AddVector(vectors[len(vectors)-1].Multiply(.2)))
path = append(path, points[len(points)-1])
- edge.IsCurve = true
edge.Route = path
// compile needs to assign edge label positions
if edge.Attributes.Label.Value != "" {
diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go
index e5ceab8aa..5c5ce2d3b 100644
--- a/d2renderers/d2sketch/sketch.go
+++ b/d2renderers/d2sketch/sketch.go
@@ -3,6 +3,8 @@ package d2sketch
import (
"encoding/json"
"fmt"
+ "regexp"
+ "strings"
_ "embed"
@@ -31,6 +33,8 @@ fillStyle: "solid",
bowing: 2,
seed: 1,`
+var floatRE = regexp.MustCompile(`(\d+)\.(\d+)`)
+
func (r *Runner) run(js string) (goja.Value, error) {
vm := (*goja.Runtime)(r)
return vm.RunString(js)
@@ -74,7 +78,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
strokeWidth: %d,
%s
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
- paths, err := computeRoughPaths(r, js)
+ paths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -83,7 +87,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape)
pathEl.Class = "shape"
- pathEl.Style = svg_style.ShapeStyle(shape)
+ pathEl.Style = shape.CSSStyle()
for _, p := range paths {
pathEl.D = p
output += pathEl.Render()
@@ -109,7 +113,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
strokeWidth: %d,
%s
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
- paths, err := computeRoughPaths(r, js)
+ paths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -118,7 +122,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape)
pathEl.Class = "shape"
- pathEl.Style = svg_style.ShapeStyle(shape)
+ pathEl.Style = shape.CSSStyle()
for _, p := range paths {
pathEl.D = p
output += pathEl.Render()
@@ -150,14 +154,14 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
strokeWidth: %d,
%s
});`, path, shape.StrokeWidth, baseRoughProps)
- sketchPaths, err := computeRoughPaths(r, js)
+ sketchPaths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
pathEl := svg_style.NewThemableElement("path")
pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape)
pathEl.Class = "shape"
- pathEl.Style = svg_style.ShapeStyle(shape)
+ pathEl.Style = shape.CSSStyle()
for _, p := range sketchPaths {
pathEl.D = p
output += pathEl.Render()
@@ -182,7 +186,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
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)
- paths, err := computeRoughPaths(r, js)
+ paths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -191,7 +195,7 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
pathEl.Fill = color.None
pathEl.Stroke = svg_style.ConnectionTheme(connection)
pathEl.Class = "connection"
- pathEl.Style = svg_style.ConnectionStyle(connection)
+ pathEl.Style = connection.CSSStyle()
pathEl.Attributes = attrs
for _, p := range paths {
pathEl.D = p
@@ -209,7 +213,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
strokeWidth: %d,
%s
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
- paths, err := computeRoughPaths(r, js)
+ paths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -217,7 +221,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape)
pathEl.Class = "shape"
- pathEl.Style = svg_style.ShapeStyle(shape)
+ pathEl.Style = shape.CSSStyle()
for _, p := range paths {
pathEl.D = p
output += pathEl.Render()
@@ -235,7 +239,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
fill: "#000",
%s
});`, shape.Width, rowHeight, baseRoughProps)
- paths, err = computeRoughPaths(r, js)
+ paths, err = computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -315,7 +319,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
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)
- paths, err = computeRoughPaths(r, js)
+ paths, err = computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -348,7 +352,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
strokeWidth: %d,
%s
});`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
- paths, err := computeRoughPaths(r, js)
+ paths, err := computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -356,6 +360,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape)
pathEl.Class = "shape"
+ pathEl.Style = shape.CSSStyle()
for _, p := range paths {
pathEl.D = p
output += pathEl.Render()
@@ -374,7 +379,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
fill: "#000",
%s
});`, shape.Width, headerBox.Height, baseRoughProps)
- paths, err = computeRoughPaths(r, js)
+ paths, err = computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -428,7 +433,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
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)
- paths, err = computeRoughPaths(r, js)
+ paths, err = computeRoughPathData(r, js)
if err != nil {
return "", err
}
@@ -487,38 +492,242 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
return output
}
-func computeRoughPaths(r *Runner, js string) ([]string, error) {
+func computeRoughPathData(r *Runner, js string) ([]string, error) {
if _, err := r.run(js); err != nil {
return nil, err
}
- return extractPaths(r)
+ roughPaths, err := extractRoughPaths(r)
+ if err != nil {
+ return nil, err
+ }
+ return extractPathData(roughPaths)
+}
+
+func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
+ if _, err := r.run(js); err != nil {
+ return nil, err
+ }
+ return extractRoughPaths(r)
}
type attrs struct {
D string `json:"d"`
}
-type node struct {
- Attrs attrs `json:"attrs"`
+type style struct {
+ Stroke string `json:"stroke,omitempty"`
+ StrokeWidth string `json:"strokeWidth,omitempty"`
+ Fill string `json:"fill,omitempty"`
}
-func extractPaths(r *Runner) ([]string, error) {
- val, err := r.run("JSON.stringify(node.children)")
+type roughPath struct {
+ Attrs attrs `json:"attrs"`
+ Style style `json:"style"`
+}
+
+func (rp roughPath) StyleCSS() string {
+ style := ""
+ if rp.Style.Fill != "" {
+ style += fmt.Sprintf("fill:%s;", rp.Style.Fill)
+ }
+ if rp.Style.Stroke != "" {
+ style += fmt.Sprintf("stroke:%s;", rp.Style.Stroke)
+ }
+ if rp.Style.StrokeWidth != "" {
+ style += fmt.Sprintf("stroke-width:%s;", rp.Style.StrokeWidth)
+ }
+ return style
+}
+
+func extractRoughPaths(r *Runner) ([]roughPath, error) {
+ val, err := r.run("JSON.stringify(node.children, null, ' ')")
if err != nil {
return nil, err
}
- var nodes []node
-
- err = json.Unmarshal([]byte(val.String()), &nodes)
+ var roughPaths []roughPath
+ err = json.Unmarshal([]byte(val.String()), &roughPaths)
if err != nil {
return nil, err
}
+ // 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]
+ })
+ }
+
+ return roughPaths, nil
+}
+
+func extractPathData(roughPaths []roughPath) ([]string, error) {
var paths []string
- for _, n := range nodes {
- paths = append(paths, n.Attrs.D)
+ for _, rp := range roughPaths {
+ paths = append(paths, rp.Attrs.D)
}
-
return paths, nil
}
+
+func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
+ // 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: "white", fillStyle: "solid", seed: 1 })`,
+ `[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`,
+ strokeWidth,
+ stroke,
+ )
+ 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"`,
+ 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"`,
+ strokeWidth,
+ stroke,
+ stroke,
+ )
+ extraJS = fmt.Sprintf(
+ `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "white", fillStyle: "solid", fillWeight: 1, seed: 4 })`,
+ strokeWidth,
+ stroke,
+ )
+ case d2target.CfOneRequired:
+ arrowJS = fmt.Sprintf(
+ `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"`,
+ strokeWidth,
+ stroke,
+ stroke,
+ )
+ case d2target.CfOne:
+ arrowJS = fmt.Sprintf(
+ `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 3 })`,
+ `"M-10,-10 -10,10"`,
+ strokeWidth,
+ stroke,
+ stroke,
+ )
+ extraJS = fmt.Sprintf(
+ `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "white", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
+ strokeWidth,
+ stroke,
+ )
+ }
+ return
+}
+
+func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
+ arrowPaths := []string{}
+
+ if connection.SrcArrow != d2target.NoArrowhead {
+ arrowJS, extraJS := ArrowheadJS(r, connection.SrcArrow, connection.Stroke, connection.StrokeWidth)
+ 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,
+ )
+
+ 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...)
+ }
+
+ for _, rp := range roughPaths {
+ pathStr := fmt.Sprintf(``,
+ rp.Attrs.D,
+ rp.StyleCSS(),
+ transform,
+ )
+ arrowPaths = append(arrowPaths, pathStr)
+ }
+ }
+
+ if connection.DstArrow != d2target.NoArrowhead {
+ arrowJS, extraJS := ArrowheadJS(r, connection.DstArrow, connection.Stroke, connection.StrokeWidth)
+ 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,
+ )
+
+ 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...)
+ }
+
+ for _, rp := range roughPaths {
+ pathStr := fmt.Sprintf(``,
+ rp.Attrs.D,
+ rp.StyleCSS(),
+ transform,
+ )
+ arrowPaths = append(arrowPaths, pathStr)
+ }
+ }
+
+ return strings.Join(arrowPaths, " "), nil
+}
diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go
index af821700a..638d0d320 100644
--- a/d2renderers/d2sketch/sketch_test.go
+++ b/d2renderers/d2sketch/sketch_test.go
@@ -39,6 +39,11 @@ func TestSketch(t *testing.T) {
script: `winter.snow -> summer.sun
`,
},
+ {
+ name: "animated",
+ script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
+ `,
+ },
{
name: "connection label",
script: `a -> b: hello
@@ -275,6 +280,52 @@ shipments.order_id <-> orders.id`,
+getJobs(): "Job[]"
+setTimeout(seconds int)
}
+`,
+ },
+ {
+ name: "arrowheads",
+ script: `
+a: ""
+b: ""
+a.1 -- b.1: none
+a.2 <-> b.2: arrow {
+ source-arrowhead.shape: arrow
+ target-arrowhead.shape: arrow
+}
+a.3 <-> b.3: triangle {
+ source-arrowhead.shape: triangle
+ target-arrowhead.shape: triangle
+}
+a.4 <-> b.4: diamond {
+ source-arrowhead.shape: diamond
+ target-arrowhead.shape: diamond
+}
+a.5 <-> b.5: diamond filled {
+ source-arrowhead: {
+ shape: diamond
+ style.filled: true
+ }
+ target-arrowhead: {
+ shape: diamond
+ style.filled: true
+ }
+}
+a.6 <-> b.6: cf-many {
+ source-arrowhead.shape: cf-many
+ target-arrowhead.shape: cf-many
+}
+a.7 <-> b.7: cf-many-required {
+ source-arrowhead.shape: cf-many-required
+ target-arrowhead.shape: cf-many-required
+}
+a.8 <-> b.8: cf-one {
+ source-arrowhead.shape: cf-one
+ target-arrowhead.shape: cf-one
+}
+a.9 <-> b.9: cf-one-required {
+ source-arrowhead.shape: cf-one-required
+ target-arrowhead.shape: cf-one-required
+}
`,
},
{
diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
index efb2c8aba..8496e2c0f 100644
--- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
@@ -51,7 +51,7 @@ width="1597" height="835" viewBox="-102 -102 1597 835">
+
+
+
+
+wintersummertreessnowsun
+
+
+
\ No newline at end of file
diff --git a/d2renderers/d2sketch/testdata/arrowheads/sketch.exp.svg b/d2renderers/d2sketch/testdata/arrowheads/sketch.exp.svg
new file mode 100644
index 000000000..05f0b4575
--- /dev/null
+++ b/d2renderers/d2sketch/testdata/arrowheads/sketch.exp.svg
@@ -0,0 +1,79 @@
+
+
\ No newline at end of file
diff --git a/d2renderers/d2sketch/testdata/basic/sketch.exp.svg b/d2renderers/d2sketch/testdata/basic/sketch.exp.svg
index fa9cb40e1..085125e38 100644
--- a/d2renderers/d2sketch/testdata/basic/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/basic/sketch.exp.svg
@@ -51,7 +51,7 @@ width="319" height="556" viewBox="-102 -102 319 556">
+
+# 2 leading, 2 trailing
+def hello():
+
+ print "world"
+
+
+
+# 2 leading
+def hello():
+
+ print "world"# 2 trailing
+def hello():
+
+ print "world"
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/code_leading_trailing_newlines/elk/board.exp.json b/e2etests/testdata/regression/code_leading_trailing_newlines/elk/board.exp.json
new file mode 100644
index 000000000..da4f01ab5
--- /dev/null
+++ b/e2etests/testdata/regression/code_leading_trailing_newlines/elk/board.exp.json
@@ -0,0 +1,124 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hello world",
+ "type": "code",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 239,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0A0F25",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "\n\n# 2 leading, 2 trailing\ndef hello():\n\n print \"world\"\n\n",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "python",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 239,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "no trailing",
+ "type": "code",
+ "pos": {
+ "x": 271,
+ "y": 28
+ },
+ "width": 160,
+ "height": 118,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0A0F25",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "\n\n# 2 leading\ndef hello():\n\n print \"world\"",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "python",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 160,
+ "labelHeight": 118,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "no leading",
+ "type": "code",
+ "pos": {
+ "x": 451,
+ "y": 28
+ },
+ "width": 160,
+ "height": 118,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0A0F25",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "# 2 trailing\ndef hello():\n\n print \"world\"\n\n",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "python",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 160,
+ "labelHeight": 118,
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/regression/code_leading_trailing_newlines/elk/sketch.exp.svg b/e2etests/testdata/regression/code_leading_trailing_newlines/elk/sketch.exp.svg
new file mode 100644
index 000000000..5f47f871b
--- /dev/null
+++ b/e2etests/testdata/regression/code_leading_trailing_newlines/elk/sketch.exp.svg
@@ -0,0 +1,69 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json
new file mode 100644
index 000000000..5a05fac5f
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/board.exp.json
@@ -0,0 +1,392 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 571,
+ "height": 648,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#E3E9FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 17,
+ "labelHeight": 41,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "a.c",
+ "type": "",
+ "pos": {
+ "x": 40,
+ "y": 359
+ },
+ "width": 478,
+ "height": 235,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "white",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "c",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 15,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.c.d",
+ "type": "",
+ "pos": {
+ "x": 236,
+ "y": 413
+ },
+ "width": 114,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 14,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "a.b",
+ "type": "",
+ "pos": {
+ "x": 64,
+ "y": 55
+ },
+ "width": 113,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "b",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 13,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.1",
+ "type": "",
+ "pos": {
+ "x": 237,
+ "y": 55
+ },
+ "width": 112,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "1",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 12,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.2",
+ "type": "",
+ "pos": {
+ "x": 409,
+ "y": 55
+ },
+ "width": 113,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "2",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 13,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "a.(b -> c)[0]",
+ "src": "a.b",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "diamond",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "red",
+ "label": "line 1\nline 2\nline 3\nline 4",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "red",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 69,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 120,
+ "y": 181.5
+ },
+ {
+ "x": 120,
+ "y": 251.9
+ },
+ {
+ "x": 120,
+ "y": 287.5
+ },
+ {
+ "x": 120,
+ "y": 359.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "a.(1 -> c)[0]",
+ "src": "a.1",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 292.5,
+ "y": 181.5
+ },
+ {
+ "x": 292.5,
+ "y": 251.9
+ },
+ {
+ "x": 292.5,
+ "y": 287.5
+ },
+ {
+ "x": 292.5,
+ "y": 359.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "a.(2 <-> c)[0]",
+ "src": "a.2",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 465,
+ "y": 181.5
+ },
+ {
+ "x": 465,
+ "y": 251.9
+ },
+ {
+ "x": 465,
+ "y": 287.5
+ },
+ {
+ "x": 465,
+ "y": 359.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg
new file mode 100644
index 000000000..657dbb9f6
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_broken_arrowhead/dagre/sketch.exp.svg
@@ -0,0 +1,66 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/elk/board.exp.json b/e2etests/testdata/regression/dagre_broken_arrowhead/elk/board.exp.json
new file mode 100644
index 000000000..7c00e2724
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_broken_arrowhead/elk/board.exp.json
@@ -0,0 +1,381 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 490,
+ "height": 878,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#E3E9FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 17,
+ "labelHeight": 41,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "a.c",
+ "type": "",
+ "pos": {
+ "x": 106,
+ "y": 539
+ },
+ "width": 264,
+ "height": 276,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "white",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "c",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 15,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.c.d",
+ "type": "",
+ "pos": {
+ "x": 181,
+ "y": 614
+ },
+ "width": 114,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 14,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "a.b",
+ "type": "",
+ "pos": {
+ "x": 87,
+ "y": 87
+ },
+ "width": 113,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "b",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 13,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.1",
+ "type": "",
+ "pos": {
+ "x": 182,
+ "y": 313
+ },
+ "width": 112,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "1",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 12,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "a.2",
+ "type": "",
+ "pos": {
+ "x": 314,
+ "y": 313
+ },
+ "width": 113,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#EDF0FD",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "2",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 13,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "a.(b -> c)[0]",
+ "src": "a.b",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "diamond",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "red",
+ "label": "line 1\nline 2\nline 3\nline 4",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "red",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 69,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 143.5,
+ "y": 213
+ },
+ {
+ "x": 143.5,
+ "y": 489
+ },
+ {
+ "x": 172.5,
+ "y": 489
+ },
+ {
+ "x": 172.5,
+ "y": 539
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "a.(1 -> c)[0]",
+ "src": "a.1",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 238.5,
+ "y": 439
+ },
+ {
+ "x": 238.5,
+ "y": 539
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "a.(2 <-> c)[0]",
+ "src": "a.2",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "a.c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 371,
+ "y": 439
+ },
+ {
+ "x": 371,
+ "y": 489
+ },
+ {
+ "x": 304.5,
+ "y": 489
+ },
+ {
+ "x": 304.5,
+ "y": 539
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/regression/dagre_broken_arrowhead/elk/sketch.exp.svg b/e2etests/testdata/regression/dagre_broken_arrowhead/elk/sketch.exp.svg
new file mode 100644
index 000000000..1160dee24
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_broken_arrowhead/elk/sketch.exp.svg
@@ -0,0 +1,66 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/animated/dagre/board.exp.json b/e2etests/testdata/stable/animated/dagre/board.exp.json
new file mode 100644
index 000000000..0acdeb9d5
--- /dev/null
+++ b/e2etests/testdata/stable/animated/dagre/board.exp.json
@@ -0,0 +1,912 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "your love life will be",
+ "type": "",
+ "pos": {
+ "x": 58,
+ "y": 0
+ },
+ "width": 247,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "your love life will be",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 147,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "happy",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 247
+ },
+ "width": 149,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "happy",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 49,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "harmonious",
+ "type": "",
+ "pos": {
+ "x": 209,
+ "y": 247
+ },
+ "width": 190,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "harmonious",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 90,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "boredom",
+ "type": "",
+ "pos": {
+ "x": 471,
+ "y": 247
+ },
+ "width": 168,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "boredom",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "immortality",
+ "type": "",
+ "pos": {
+ "x": 460,
+ "y": 0
+ },
+ "width": 191,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "immortality",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 91,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Friday",
+ "type": "",
+ "pos": {
+ "x": 711,
+ "y": 0
+ },
+ "width": 150,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Friday",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Monday",
+ "type": "",
+ "pos": {
+ "x": 705,
+ "y": 247
+ },
+ "width": 161,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Monday",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 61,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Insomnia",
+ "type": "",
+ "pos": {
+ "x": 1126,
+ "y": 0
+ },
+ "width": 170,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Insomnia",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 70,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Sleep",
+ "type": "",
+ "pos": {
+ "x": 930,
+ "y": 247
+ },
+ "width": 145,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Sleep",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 45,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Wake",
+ "type": "",
+ "pos": {
+ "x": 1135,
+ "y": 247
+ },
+ "width": 144,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Wake",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Dream",
+ "type": "",
+ "pos": {
+ "x": 1339,
+ "y": 247
+ },
+ "width": 151,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Dream",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Listen",
+ "type": "",
+ "pos": {
+ "x": 1552,
+ "y": 0
+ },
+ "width": 148,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Listen",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 48,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Talk",
+ "type": "",
+ "pos": {
+ "x": 1558,
+ "y": 247
+ },
+ "width": 136,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Talk",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(your love life will be -> happy)[0]",
+ "src": "your love life will be",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "happy",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 126.917004048583,
+ "y": 126
+ },
+ {
+ "x": 84.9834008097166,
+ "y": 174.4
+ },
+ {
+ "x": 74.5,
+ "y": 198.7
+ },
+ {
+ "x": 74.5,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(your love life will be -> harmonious)[0]",
+ "src": "your love life will be",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "harmonious",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 243.98987854251013,
+ "y": 126
+ },
+ {
+ "x": 291.99797570850205,
+ "y": 174.4
+ },
+ {
+ "x": 304,
+ "y": 198.7
+ },
+ {
+ "x": 304,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(boredom <- immortality)[0]",
+ "src": "boredom",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "immortality",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 555.25,
+ "y": 247
+ },
+ {
+ "x": 555.25,
+ "y": 198.6
+ },
+ {
+ "x": 555.25,
+ "y": 174.3
+ },
+ {
+ "x": 555.25,
+ "y": 125.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Friday <-> Monday)[0]",
+ "src": "Friday",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "Monday",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 785.75,
+ "y": 126
+ },
+ {
+ "x": 785.75,
+ "y": 174.4
+ },
+ {
+ "x": 785.75,
+ "y": 198.7
+ },
+ {
+ "x": 785.75,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Sleep)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Sleep",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1126,
+ "y": 113.2874251497006
+ },
+ {
+ "x": 1027,
+ "y": 171.8574850299401
+ },
+ {
+ "x": 1002.25,
+ "y": 198.7
+ },
+ {
+ "x": 1002.25,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Wake)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Wake",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1208.831983805668,
+ "y": 126
+ },
+ {
+ "x": 1207.1663967611337,
+ "y": 174.4
+ },
+ {
+ "x": 1206.75,
+ "y": 198.7
+ },
+ {
+ "x": 1206.75,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Dream)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Dream",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 8,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1296,
+ "y": 114.64821648216483
+ },
+ {
+ "x": 1390.6,
+ "y": 172.12964329643296
+ },
+ {
+ "x": 1414.25,
+ "y": 198.7
+ },
+ {
+ "x": 1414.25,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Listen <-> Talk)[0]",
+ "src": "Listen",
+ "srcArrow": "cf-one",
+ "srcLabel": "",
+ "dst": "Talk",
+ "dstArrow": "diamond",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "hear",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 32,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1625.5,
+ "y": 126
+ },
+ {
+ "x": 1625.5,
+ "y": 174.4
+ },
+ {
+ "x": 1625.5,
+ "y": 198.7
+ },
+ {
+ "x": 1625.5,
+ "y": 247.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
new file mode 100644
index 000000000..472f2ae90
--- /dev/null
+++ b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
@@ -0,0 +1,65 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/animated/elk/board.exp.json b/e2etests/testdata/stable/animated/elk/board.exp.json
new file mode 100644
index 000000000..92badcbf0
--- /dev/null
+++ b/e2etests/testdata/stable/animated/elk/board.exp.json
@@ -0,0 +1,864 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "your love life will be",
+ "type": "",
+ "pos": {
+ "x": 111,
+ "y": 12
+ },
+ "width": 247,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "your love life will be",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 147,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "happy",
+ "type": "",
+ "pos": {
+ "x": 12,
+ "y": 238
+ },
+ "width": 149,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "happy",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 49,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "harmonious",
+ "type": "",
+ "pos": {
+ "x": 181,
+ "y": 238
+ },
+ "width": 190,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "harmonious",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 90,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "boredom",
+ "type": "",
+ "pos": {
+ "x": 402,
+ "y": 12
+ },
+ "width": 168,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "boredom",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "immortality",
+ "type": "",
+ "pos": {
+ "x": 391,
+ "y": 238
+ },
+ "width": 191,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "immortality",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 91,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Friday",
+ "type": "",
+ "pos": {
+ "x": 607,
+ "y": 12
+ },
+ "width": 150,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Friday",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Monday",
+ "type": "",
+ "pos": {
+ "x": 602,
+ "y": 238
+ },
+ "width": 161,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Monday",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 61,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Insomnia",
+ "type": "",
+ "pos": {
+ "x": 935,
+ "y": 12
+ },
+ "width": 170,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Insomnia",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 70,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Sleep",
+ "type": "",
+ "pos": {
+ "x": 783,
+ "y": 238
+ },
+ "width": 145,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Sleep",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 45,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Wake",
+ "type": "",
+ "pos": {
+ "x": 948,
+ "y": 238
+ },
+ "width": 144,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Wake",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Dream",
+ "type": "",
+ "pos": {
+ "x": 1112,
+ "y": 238
+ },
+ "width": 151,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Dream",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Listen",
+ "type": "",
+ "pos": {
+ "x": 1225,
+ "y": 12
+ },
+ "width": 148,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Listen",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 48,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "Talk",
+ "type": "",
+ "pos": {
+ "x": 1231,
+ "y": 464
+ },
+ "width": 136,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Talk",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(your love life will be -> happy)[0]",
+ "src": "your love life will be",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "happy",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 193.66666666666652,
+ "y": 138
+ },
+ {
+ "x": 193.66666666666652,
+ "y": 188
+ },
+ {
+ "x": 86.5,
+ "y": 188
+ },
+ {
+ "x": 86.5,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(your love life will be -> harmonious)[0]",
+ "src": "your love life will be",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "harmonious",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 275.9999999999999,
+ "y": 138
+ },
+ {
+ "x": 276,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(boredom <- immortality)[0]",
+ "src": "boredom",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "immortality",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 486.5,
+ "y": 138
+ },
+ {
+ "x": 486.5,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Friday <-> Monday)[0]",
+ "src": "Friday",
+ "srcArrow": "triangle",
+ "srcLabel": "",
+ "dst": "Monday",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 682.5,
+ "y": 138
+ },
+ {
+ "x": 682.5,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Sleep)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Sleep",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 977.5,
+ "y": 138
+ },
+ {
+ "x": 977.5,
+ "y": 188
+ },
+ {
+ "x": 855.5,
+ "y": 188
+ },
+ {
+ "x": 855.5,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Wake)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Wake",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1020,
+ "y": 138
+ },
+ {
+ "x": 1020,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Insomnia -- Dream)[0]",
+ "src": "Insomnia",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "Dream",
+ "dstArrow": "none",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 8,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1062.5,
+ "y": 138
+ },
+ {
+ "x": 1062.5,
+ "y": 188
+ },
+ {
+ "x": 1187.5,
+ "y": 188
+ },
+ {
+ "x": 1187.5,
+ "y": 238
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(Listen <-> Talk)[0]",
+ "src": "Listen",
+ "srcArrow": "cf-one",
+ "srcLabel": "",
+ "dst": "Talk",
+ "dstArrow": "diamond",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "hear",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 32,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1299,
+ "y": 138
+ },
+ {
+ "x": 1299,
+ "y": 464
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/animated/elk/sketch.exp.svg b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
new file mode 100644
index 000000000..5d3c01de3
--- /dev/null
+++ b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
@@ -0,0 +1,65 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/chaos2/dagre/board.exp.json b/e2etests/testdata/stable/chaos2/dagre/board.exp.json
index 12a1513a1..241154fec 100644
--- a/e2etests/testdata/stable/chaos2/dagre/board.exp.json
+++ b/e2etests/testdata/stable/chaos2/dagre/board.exp.json
@@ -822,20 +822,11 @@
"x": 925,
"y": 745.597137014315
},
- {
- "x": 847.4,
- "y": 758.3971370143149
- },
- {
- "x": 905.6,
- "y": 748.797137014315
- },
{
"x": 828,
"y": 761.597137014315
}
],
- "isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
@@ -978,20 +969,11 @@
"x": 912,
"y": 522
},
- {
- "x": 845,
- "y": 533.8777110844337
- },
- {
- "x": 895.25,
- "y": 524.9694277711085
- },
{
"x": 828.25,
"y": 536.8471388555422
}
],
- "isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
diff --git a/e2etests/testdata/stable/chaos2/dagre/sketch.exp.svg b/e2etests/testdata/stable/chaos2/dagre/sketch.exp.svg
index 5631cb34a..e540281c0 100644
--- a/e2etests/testdata/stable/chaos2/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/chaos2/dagre/sketch.exp.svg
@@ -796,7 +796,7 @@ width="1331" height="1939" viewBox="-102 -102 1331 1939">aabbllmmnnoocciikkddgghhjjeeff1122 334455667788
+aabbllmmnnoocciikkddgghhjjeeff1122 334455667788
diff --git a/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json b/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json
index d64f023cc..29604db23 100644
--- a/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json
+++ b/e2etests/testdata/stable/child_parent_edges/dagre/board.exp.json
@@ -361,20 +361,11 @@
"x": 243.66666666666669,
"y": 238
},
- {
- "x": 243.93333333333334,
- "y": 237.99628770301624
- },
- {
- "x": 243.73333333333335,
- "y": 237.99907192575407
- },
{
"x": 244,
"y": 237.9953596287703
}
],
- "isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
diff --git a/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg b/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg
index c157addac..f8a893097 100644
--- a/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/child_parent_edges/dagre/sketch.exp.svg
@@ -39,7 +39,7 @@ width="698" height="630" viewBox="-102 -102 698 630">xy
+
+
+I like turtlesab
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/elk/board.exp.json b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/board.exp.json
new file mode 100644
index 000000000..e869a4914
--- /dev/null
+++ b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/board.exp.json
@@ -0,0 +1,189 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "x",
+ "type": "sql_table",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 60,
+ "height": 72,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#0A0F25",
+ "stroke": "#FFFFFF",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "I like turtles",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": [
+ {
+ "name": {
+ "label": "y",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 26
+ },
+ "type": {
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "constraint": "",
+ "reference": "a.b"
+ }
+ ],
+ "label": "x",
+ "fontSize": 20,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 17,
+ "labelHeight": 36,
+ "zIndex": 0,
+ "level": 1,
+ "primaryAccentColor": "#0D32B2",
+ "secondaryAccentColor": "#4A6FF3",
+ "neutralAccentColor": "#676C7E"
+ },
+ {
+ "id": "a",
+ "type": "sql_table",
+ "pos": {
+ "x": 12,
+ "y": 184
+ },
+ "width": 60,
+ "height": 72,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#0A0F25",
+ "stroke": "#FFFFFF",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": [
+ {
+ "name": {
+ "label": "b",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 26
+ },
+ "type": {
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "constraint": "",
+ "reference": ""
+ }
+ ],
+ "label": "a",
+ "fontSize": 20,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 16,
+ "labelHeight": 36,
+ "zIndex": 0,
+ "level": 1,
+ "primaryAccentColor": "#0D32B2",
+ "secondaryAccentColor": "#4A6FF3",
+ "neutralAccentColor": "#676C7E"
+ }
+ ],
+ "connections": [
+ {
+ "id": "(x -> a)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a",
+ "dstArrow": "cf-many",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 42,
+ "y": 84
+ },
+ {
+ "x": 42,
+ "y": 184
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg
new file mode 100644
index 000000000..ef9d36cc8
--- /dev/null
+++ b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg
@@ -0,0 +1,78 @@
+
+
\ No newline at end of file
diff --git a/lib/geo/point.go b/lib/geo/point.go
index 13275db79..0acc5021b 100644
--- a/lib/geo/point.go
+++ b/lib/geo/point.go
@@ -187,12 +187,12 @@ func (p *Point) DistanceToLine(p1, p2 *Point) float64 {
// Moves the given point by Vector
func (start *Point) AddVector(v Vector) *Point {
- return start.toVector().Add(v).ToPoint()
+ return start.ToVector().Add(v).ToPoint()
}
// Creates a Vector of the size between start and endpoint, pointing to endpoint
func (start *Point) VectorTo(endpoint *Point) Vector {
- return endpoint.toVector().Minus(start.toVector())
+ return endpoint.ToVector().Minus(start.ToVector())
}
func (p *Point) FormattedCoordinates() string {
@@ -205,7 +205,7 @@ func (q *Point) OnSegment(p, r *Point) bool {
}
// Creates a Vector pointing to point
-func (endpoint *Point) toVector() Vector {
+func (endpoint *Point) ToVector() Vector {
return []float64{endpoint.X, endpoint.Y}
}
diff --git a/lib/geo/point_test.go b/lib/geo/point_test.go
index 259975897..343f8bb30 100644
--- a/lib/geo/point_test.go
+++ b/lib/geo/point_test.go
@@ -29,7 +29,7 @@ func TestAddVector(t *testing.T) {
func TestToVector(t *testing.T) {
p := &Point{3.5, 6.7}
- v := p.toVector()
+ v := p.ToVector()
if v[0] != p.X || v[1] != p.Y {
t.Fatalf("Expected Vector (%v) coordinates to match the point (%v)", p, v)
diff --git a/lib/geo/segment.go b/lib/geo/segment.go
index 473cb12e8..df6f64a5c 100644
--- a/lib/geo/segment.go
+++ b/lib/geo/segment.go
@@ -114,3 +114,11 @@ func (segment *Segment) GetBounds(segments []*Segment, buffer float64) (float64,
}
return floor, ceil
}
+
+func (segment Segment) Length() float64 {
+ return EuclideanDistance(segment.Start.X, segment.Start.Y, segment.End.X, segment.End.Y)
+}
+
+func (segment Segment) ToVector() Vector {
+ return NewVector(segment.End.X-segment.Start.X, segment.End.Y-segment.Start.Y)
+}
diff --git a/lib/geo/vector.go b/lib/geo/vector.go
index 199fe73db..db1bb107b 100644
--- a/lib/geo/vector.go
+++ b/lib/geo/vector.go
@@ -76,3 +76,15 @@ func GetUnitNormalVector(x1, y1, x2, y2 float64) (float64, float64) {
length := EuclideanDistance(x1, y1, x2, y2)
return normalX / length, normalY / length
}
+
+func (a Vector) Radians() float64 {
+ return math.Atan2(a[1], a[0])
+}
+
+func (a Vector) Degrees() float64 {
+ return a.Radians() * 180 / math.Pi
+}
+
+func (a Vector) Reverse() Vector {
+ return a.Multiply(-1)
+}
diff --git a/lib/imgbundler/imgbundler.go b/lib/imgbundler/imgbundler.go
index e1b719687..5c6de7038 100644
--- a/lib/imgbundler/imgbundler.go
+++ b/lib/imgbundler/imgbundler.go
@@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"fmt"
+ "html"
"io/ioutil"
"mime"
"net/http"
@@ -66,7 +67,7 @@ func filterImageElements(imgs [][][]byte, isRemote bool) [][][]byte {
continue
}
- u, err := url.Parse(href)
+ u, err := url.Parse(html.UnescapeString(href))
isRemoteImg := err == nil && strings.HasPrefix(u.Scheme, "http")
if isRemoteImg == isRemote {
@@ -147,9 +148,9 @@ func worker(ctx context.Context, href []byte, isRemote bool) ([]byte, error) {
var mimeType string
var err error
if isRemote {
- buf, mimeType, err = httpGet(ctx, string(href))
+ buf, mimeType, err = httpGet(ctx, html.UnescapeString(string(href)))
} else {
- buf, err = os.ReadFile(string(href))
+ buf, err = os.ReadFile(html.UnescapeString(string(href)))
}
if err != nil {
return nil, err
@@ -194,7 +195,7 @@ func httpGet(ctx context.Context, href string) ([]byte, string, error) {
func sniffMimeType(href, buf []byte, isRemote bool) string {
p := string(href)
if isRemote {
- u, err := url.Parse(p)
+ u, err := url.Parse(html.UnescapeString(p))
if err != nil {
p = ""
} else {
diff --git a/testdata/d2compiler/TestCompile/table_connection_attr.exp.json b/testdata/d2compiler/TestCompile/table_connection_attr.exp.json
new file mode 100644
index 000000000..2dc6c143d
--- /dev/null
+++ b/testdata/d2compiler/TestCompile/table_connection_attr.exp.json
@@ -0,0 +1,570 @@
+{
+ "graph": {
+ "ast": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-11:0:99",
+ "nodes": [
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-3:1:29",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-0:1:1",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-0:1:1",
+ "value": [
+ {
+ "string": "x",
+ "raw_string": "x"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {
+ "map": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:3:3-3:0:28",
+ "nodes": [
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,1:2:7-1:18:23",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,1:2:7-1:7:12",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,1:2:7-1:7:12",
+ "value": [
+ {
+ "string": "shape",
+ "raw_string": "shape"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,1:9:14-1:18:23",
+ "value": [
+ {
+ "string": "sql_table",
+ "raw_string": "sql_table"
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,2:2:26-2:3:27",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,2:2:26-2:3:27",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,2:2:26-2:3:27",
+ "value": [
+ {
+ "string": "y",
+ "raw_string": "y"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {}
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:0:30-7:1:59",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:0:30-4:1:31",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:0:30-4:1:31",
+ "value": [
+ {
+ "string": "a",
+ "raw_string": "a"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {
+ "map": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:3:33-7:0:58",
+ "nodes": [
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,5:2:37-5:18:53",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,5:2:37-5:7:42",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,5:2:37-5:7:42",
+ "value": [
+ {
+ "string": "shape",
+ "raw_string": "shape"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,5:9:44-5:18:53",
+ "value": [
+ {
+ "string": "sql_table",
+ "raw_string": "sql_table"
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,6:2:56-6:3:57",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,6:2:56-6:3:57",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,6:2:56-6:3:57",
+ "value": [
+ {
+ "string": "b",
+ "raw_string": "b"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {}
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-10:1:98",
+ "edges": [
+ {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-8:10:70",
+ "src": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-8:4:64",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-8:1:61",
+ "value": [
+ {
+ "string": "x",
+ "raw_string": "x"
+ }
+ ]
+ }
+ },
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:2:62-8:3:63",
+ "value": [
+ {
+ "string": "y",
+ "raw_string": "y"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "src_arrow": "",
+ "dst": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:6:66-8:10:70",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:7:67-8:8:68",
+ "value": [
+ {
+ "string": "a",
+ "raw_string": "a"
+ }
+ ]
+ }
+ },
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:9:69-8:10:70",
+ "value": [
+ {
+ "string": "b",
+ "raw_string": "b"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "dst_arrow": ">"
+ }
+ ],
+ "primary": {},
+ "value": {
+ "map": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:12:72-10:0:97",
+ "nodes": [
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,9:2:76-9:22:96",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,9:2:76-9:16:90",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,9:2:76-9:7:81",
+ "value": [
+ {
+ "string": "style",
+ "raw_string": "style"
+ }
+ ]
+ }
+ },
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,9:8:82-9:16:90",
+ "value": [
+ {
+ "string": "animated",
+ "raw_string": "animated"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {
+ "boolean": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,9:18:92-9:22:96",
+ "value": true
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ "root": {
+ "id": "",
+ "id_val": "",
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "attributes": {
+ "label": {
+ "value": ""
+ },
+ "style": {},
+ "near_key": null,
+ "shape": {
+ "value": ""
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ },
+ "edges": [
+ {
+ "index": 0,
+ "minWidth": 0,
+ "minHeight": 0,
+ "srcTableColumnIndex": 0,
+ "dstTableColumnIndex": 0,
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "isCurve": false,
+ "src_arrow": false,
+ "dst_arrow": true,
+ "references": [
+ {
+ "map_key_edge_index": 0
+ }
+ ],
+ "attributes": {
+ "label": {
+ "value": ""
+ },
+ "style": {
+ "animated": {
+ "value": "true"
+ }
+ },
+ "near_key": null,
+ "shape": {
+ "value": ""
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ }
+ ],
+ "objects": [
+ {
+ "id": "x",
+ "id_val": "x",
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "references": [
+ {
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-0:1:1",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,0:0:0-0:1:1",
+ "value": [
+ {
+ "string": "x",
+ "raw_string": "x"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "key_path_index": 0,
+ "map_key_edge_index": 0
+ },
+ {
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-8:4:64",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:0:60-8:1:61",
+ "value": [
+ {
+ "string": "x",
+ "raw_string": "x"
+ }
+ ]
+ }
+ },
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:2:62-8:3:63",
+ "value": [
+ {
+ "string": "y",
+ "raw_string": "y"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "key_path_index": 0,
+ "map_key_edge_index": 0
+ }
+ ],
+ "sql_table": {
+ "columns": [
+ {
+ "name": {
+ "label": "y",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "type": {
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "constraint": "",
+ "reference": "a.b"
+ }
+ ]
+ },
+ "attributes": {
+ "label": {
+ "value": "x"
+ },
+ "style": {},
+ "near_key": null,
+ "shape": {
+ "value": "sql_table"
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ },
+ {
+ "id": "a",
+ "id_val": "a",
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "references": [
+ {
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:0:30-4:1:31",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,4:0:30-4:1:31",
+ "value": [
+ {
+ "string": "a",
+ "raw_string": "a"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "key_path_index": 0,
+ "map_key_edge_index": 0
+ },
+ {
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:6:66-8:10:70",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:7:67-8:8:68",
+ "value": [
+ {
+ "string": "a",
+ "raw_string": "a"
+ }
+ ]
+ }
+ },
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/table_connection_attr.d2,8:9:69-8:10:70",
+ "value": [
+ {
+ "string": "b",
+ "raw_string": "b"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "key_path_index": 0,
+ "map_key_edge_index": 0
+ }
+ ],
+ "sql_table": {
+ "columns": [
+ {
+ "name": {
+ "label": "b",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "type": {
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0
+ },
+ "constraint": "",
+ "reference": ""
+ }
+ ]
+ },
+ "attributes": {
+ "label": {
+ "value": "a"
+ },
+ "style": {},
+ "near_key": null,
+ "shape": {
+ "value": "sql_table"
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ }
+ ]
+ },
+ "err": null
+}