From a7f83f702981079333fd3dfd413286ce9fbcc8f5 Mon Sep 17 00:00:00 2001 From: Daniel Suh Date: Tue, 14 May 2024 16:10:19 -0400 Subject: [PATCH 01/12] bezier curves work? --- d2renderers/d2svg/d2svg.go | 210 ++++++- e2etests/testdata/txtar.txt | 7 + .../dagre/board.exp.json | 571 ++++++++++++++++++ .../dagre/sketch.exp.svg | 107 ++++ .../elk/board.exp.json | 558 +++++++++++++++++ .../elk/sketch.exp.svg | 107 ++++ 6 files changed, 1559 insertions(+), 1 deletion(-) create mode 100644 e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json create mode 100644 e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json create mode 100644 e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 6906c4d68..707a922df 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -11,6 +11,7 @@ import ( "html" "io" "sort" + "strconv" "strings" "math" @@ -494,6 +495,192 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin ) } +func splitBezierCurve(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { + u0, u1 := 1 - t0, 1 - t1 + + q1 := geo.Point { + X: (u0 * u0 * u0) * p1.X + (3 * t0 * u0 * u0) * p2.X + (3 * t0 * t0 * u0) * p3.X + t0 * t0 * t0 * p4.X, + Y: (u0 * u0 * u0) * p1.Y + (3 * t0 * u0 * u0) * p2.Y + (3 * t0 * t0 * u0) * p3.Y + t0 * t0 * t0 * p4.Y, + } + q2 := geo.Point { + X: (u0 * u0 * u1) * p1.X + (2 * t0 * u0 * u1 + u0 * u0 * t1) * p2.X + (t0 * t0 * u1 + 2 * u0 * t0 * t1) * p3.X + t0 * t0 * t1 * p4.X, + Y: (u0 * u0 * u1) * p1.Y + (2 * t0 * u0 * u1 + u0 * u0 * t1) * p2.Y + (t0 * t0 * u1 + 2 * u0 * t0 * t1) * p3.Y + t0 * t0 * t1 * p4.Y, + } + q3 := geo.Point{ + X: (u0 * u1 * u1) * p1.X + (t0 * u1 * u1 + 2 * u0 * t1 * u1) * p2.X + (2 * t0 * t1 * u1 + u0 * t1 * t1) * p3.X + t0 * t1 * t1 * p4.X, + Y: (u0 * u1 * u1) * p1.Y + (t0 * u1 * u1 + 2 * u0 * t1 * u1) * p2.Y + (2 * t0 * t1 * u1 + u0 * t1 * t1) * p3.Y + t0 * t1 * t1 * p4.Y, + } + q4 := geo.Point{ + X: (u1 * u1 * u1) * p1.X + (3 * t1 * u1 * u1) * p2.X + (3 * t1 * t1 * u1) * p3.X + t1 * t1 * t1 * p4.X, + Y: (u1 * u1 * u1) * p1.Y + (3 * t1 * u1 * u1) * p2.Y + (3 * t1 * t1 * u1) * p3.Y + t1 * t1 * t1 * p4.Y, + } + + return q1, q2, q3, q4 +} + +func splitPath(path string, percentage float64) (string, string) { + var sumPathLens, curPathLen, x, y, pathLength float64 + var prevPosition geo.Point + var path1, path2 string + var increment int + + pathData := strings.Split(path, " ") + + for i := 0; i < len(pathData); { + switch pathData[i] { + case "M": + x, _ = strconv.ParseFloat(pathData[i + 1], 64) + y, _ = strconv.ParseFloat(pathData[i + 2], 64) + + increment = 3 + case "L": + x, _ = strconv.ParseFloat(pathData[i + 1], 64) + y, _ = strconv.ParseFloat(pathData[i + 2], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 3 + case "C": + x, _ = strconv.ParseFloat(pathData[i + 5], 64) + y, _ = strconv.ParseFloat(pathData[i + 6], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 7 + case "S": + x, _ = strconv.ParseFloat(pathData[i + 3], 64) + y, _ = strconv.ParseFloat(pathData[i + 4], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 5 + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + prevPosition = geo.Point{X: x, Y: y}; + i += increment; + } + fmt.Println(pathLength); + + i := 0 + + for ; i < len(pathData); { + switch pathData[i] { + case "M": + x, _ = strconv.ParseFloat(pathData[i + 1], 64) + y, _ = strconv.ParseFloat(pathData[i + 2], 64) + + if sumPathLens + curPathLen < pathLength * percentage { + path1 += fmt.Sprintf("M %s %s ", pathData[i + 1], pathData[i + 2]) + } + + increment = 3 + case "L": + x, _ = strconv.ParseFloat(pathData[i + 1], 64) + y, _ = strconv.ParseFloat(pathData[i + 2], 64) + + curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + if sumPathLens + curPathLen < pathLength * percentage { + path1 += fmt.Sprintf("L %s %s ", pathData[i + 1], pathData[i + 2]) + } + + increment = 3 + case "C": + x, _ = strconv.ParseFloat(pathData[i + 5], 64) + y, _ = strconv.ParseFloat(pathData[i + 6], 64) + + curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + if sumPathLens + curPathLen < pathLength * percentage { + path1 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4], pathData[i + 5], pathData[i + 6]); + } + + increment = 7 + case "S": + x, _ = strconv.ParseFloat(pathData[i + 3], 64) + y, _ = strconv.ParseFloat(pathData[i + 4], 64) + + curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + if sumPathLens + curPathLen < pathLength * percentage { + path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) + } + + increment = 5 + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + sumPathLens += curPathLen + + if sumPathLens >= pathLength * percentage { + t := (pathLength * percentage - sumPathLens + curPathLen) / curPathLen + fmt.Println(t) + + switch(pathData[i]) { + case "L": + path1 += fmt.Sprintf("L %f %f ", (x - prevPosition.X) * t + prevPosition.X, (y - prevPosition.Y) * t + prevPosition.Y) + path2 += fmt.Sprintf("M %f %f L %f %f ", (x - prevPosition.X) * t + prevPosition.X, (y - prevPosition.Y) * t + prevPosition.Y, x, y) + case "C": + h1x, _ := strconv.ParseFloat(pathData[i + 1], 64) + h1y, _ := strconv.ParseFloat(pathData[i + 2], 64) + h2x, _ := strconv.ParseFloat(pathData[i + 3], 64) + h2y, _ := strconv.ParseFloat(pathData[i + 4], 64) + p1x, _ := strconv.ParseFloat(pathData[i + 5], 64) + p1y, _ := strconv.ParseFloat(pathData[i + 6], 64) + + heading1 := geo.Point{X: h1x, Y: h1y} + heading2 := geo.Point{X: h2x, Y: h2y} + nextPoint := geo.Point{X: p1x, Y: p1y} + + _, q2, q3, q4 := splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) + + path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y); + + case "S": + path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + i += increment + prevPosition = geo.Point{X: x, Y: y} + break + } + + i += increment + prevPosition = geo.Point{X: x, Y: y} + } + + for ; i < len(pathData); { + switch pathData[i] { + case "M": + path2 += fmt.Sprintf("M %s %s ", pathData[i + 1], pathData[i + 2]) + increment = 3 + case "L": + path2 += fmt.Sprintf("L %s %s ", pathData[i + 1], pathData[i + 2]) + increment = 3 + case "C": + path2 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4], pathData[i + 5], pathData[i + 6]); + increment = 7 + case "S": + path2 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) + increment = 5 + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + i += increment + } + + fmt.Println(path1); + fmt.Println(path2); + + return path1, path2 +} + func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) { opacityStyle := "" if connection.Opacity != 1.0 { @@ -549,6 +736,9 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape) path := pathData(connection, srcAdj, dstAdj) mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID) + + path1, path2 := splitPath(path, 0.5); + if sketchRunner != nil { out, err := d2sketch.Connection(sketchRunner, connection, path, mask) if err != nil { @@ -575,7 +765,25 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) pathEl.Style = connection.CSSStyle() pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) - fmt.Fprint(writer, pathEl.Render()) + // fmt.Fprint(writer, pathEl.Render()) + + pathEl1 := d2themes.NewThemableElement("path") + pathEl1.D = path1 + pathEl1.Fill = color.None + pathEl1.Stroke = connection.Stroke + pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl1.Style = connection.CSSStyle() + pathEl1.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) + fmt.Fprint(writer, pathEl1.Render()) + + pathEl2 := d2themes.NewThemableElement("path") + pathEl2.D = path2 + pathEl2.Fill = color.None + pathEl2.Stroke = connection.Stroke + pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl2.Style = connection.CSSStyle() + pathEl2.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) + fmt.Fprint(writer, pathEl2.Render()) } if connection.Label != "" { diff --git a/e2etests/testdata/txtar.txt b/e2etests/testdata/txtar.txt index 60366a5a5..70a47a43b 100644 --- a/e2etests/testdata/txtar.txt +++ b/e2etests/testdata/txtar.txt @@ -213,3 +213,10 @@ ok: { } dog1 -> dog3 } + +-- bidirectional_connection_animation -- +a <-> b: {style.animated: true} +a <-> c: {style.animated: true} +a <-> d: {style.animated: true} +a <-> e: {style.animated: true} +f <-> g: {style.animated: true} diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json new file mode 100644 index 000000000..1c4c39898 --- /dev/null +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json @@ -0,0 +1,571 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "rectangle", + "pos": { + "x": 170, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b", + "type": "rectangle", + "pos": { + "x": 0, + "y": 166 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c", + "type": "rectangle", + "pos": { + "x": 113, + "y": 166 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "d", + "type": "rectangle", + "pos": { + "x": 226, + "y": 166 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "e", + "type": "rectangle", + "pos": { + "x": 340, + "y": 166 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "e", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "f", + "type": "rectangle", + "pos": { + "x": 455, + "y": 0 + }, + "width": 51, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "f", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 6, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "g", + "type": "rectangle", + "pos": { + "x": 453, + "y": 166 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "g", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(a <-> b)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 169.75, + "y": 45.957000732421875 + }, + { + "x": 55.14899826049805, + "y": 101.99099731445312 + }, + { + "x": 26.5, + "y": 126 + }, + { + "x": 26.5, + "y": 166 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> c)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 173.5, + "y": 66 + }, + { + "x": 146.3000030517578, + "y": 106 + }, + { + "x": 139.5, + "y": 126 + }, + { + "x": 139.5, + "y": 166 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> d)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "d", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 219, + "y": 66 + }, + { + "x": 246.1999969482422, + "y": 106 + }, + { + "x": 253, + "y": 126 + }, + { + "x": 253, + "y": 166 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> e)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "e", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 222.5, + "y": 46 + }, + { + "x": 337.70001220703125, + "y": 102 + }, + { + "x": 366.5, + "y": 126 + }, + { + "x": 366.5, + "y": 166 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(f <-> g)[0]", + "src": "f", + "srcArrow": "triangle", + "dst": "g", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 480, + "y": 66 + }, + { + "x": 480, + "y": 106 + }, + { + "x": 480, + "y": 126 + }, + { + "x": 480, + "y": 166 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg new file mode 100644 index 000000000..8bf7169d7 --- /dev/null +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg @@ -0,0 +1,107 @@ +abcdefg + + + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json new file mode 100644 index 000000000..e121ae7ac --- /dev/null +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json @@ -0,0 +1,558 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "rectangle", + "pos": { + "x": 68, + "y": 12 + }, + "width": 160, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b", + "type": "rectangle", + "pos": { + "x": 12, + "y": 208 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c", + "type": "rectangle", + "pos": { + "x": 85, + "y": 208 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "d", + "type": "rectangle", + "pos": { + "x": 158, + "y": 208 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "e", + "type": "rectangle", + "pos": { + "x": 232, + "y": 208 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "e", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "f", + "type": "rectangle", + "pos": { + "x": 306, + "y": 12 + }, + "width": 51, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "f", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 6, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "g", + "type": "rectangle", + "pos": { + "x": 305, + "y": 208 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "g", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(a <-> b)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 100.25, + "y": 78 + }, + { + "x": 100.25, + "y": 118 + }, + { + "x": 38.5, + "y": 118 + }, + { + "x": 38.5, + "y": 208 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> c)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 132.25, + "y": 78 + }, + { + "x": 132.25, + "y": 168 + }, + { + "x": 111.5, + "y": 168 + }, + { + "x": 111.5, + "y": 208 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> d)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "d", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 164.25, + "y": 78 + }, + { + "x": 164.25, + "y": 168 + }, + { + "x": 185, + "y": 168 + }, + { + "x": 185, + "y": 208 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a <-> e)[0]", + "src": "a", + "srcArrow": "triangle", + "dst": "e", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 196.25, + "y": 78 + }, + { + "x": 196.25, + "y": 118 + }, + { + "x": 258.5, + "y": 118 + }, + { + "x": 258.5, + "y": 208 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(f <-> g)[0]", + "src": "f", + "srcArrow": "triangle", + "dst": "g", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 332, + "y": 78 + }, + { + "x": 332, + "y": 208 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg new file mode 100644 index 000000000..eb9196aad --- /dev/null +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg @@ -0,0 +1,107 @@ +abcdefg + + + + + + + + + \ No newline at end of file From 9544a48f80bc5297e2a01f1be14485f6c9ab8b6b Mon Sep 17 00:00:00 2001 From: Daniel Suh Date: Tue, 14 May 2024 16:28:44 -0400 Subject: [PATCH 02/12] seems to be working --- d2renderers/d2svg/d2svg.go | 63 +++++++++++-------- .../dagre/sketch.exp.svg | 2 +- .../elk/sketch.exp.svg | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 707a922df..9dc4b7acd 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -635,12 +635,17 @@ func splitPath(path string, percentage float64) (string, string) { heading2 := geo.Point{X: h2x, Y: h2y} nextPoint := geo.Point{X: p1x, Y: p1y} - _, q2, q3, q4 := splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) + q1, q2, q3, q4 := splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y); + q1, q2, q3, q4 = splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) + + path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y); + case "S": path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) + path2 += fmt.Sprintf("M %s %s ", pathData[i + 3], pathData[i + 4]) default: panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) } @@ -736,8 +741,6 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape) path := pathData(connection, srcAdj, dstAdj) mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID) - - path1, path2 := splitPath(path, 0.5); if sketchRunner != nil { out, err := d2sketch.Connection(sketchRunner, connection, path, mask) @@ -758,32 +761,38 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co animatedClass = " animated-connection" } - pathEl := d2themes.NewThemableElement("path") - pathEl.D = path - pathEl.Fill = color.None - pathEl.Stroke = connection.Stroke - pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) - pathEl.Style = connection.CSSStyle() - pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) - // fmt.Fprint(writer, pathEl.Render()) + // if connection is not animated or is a directed connection + if !connection.Animated || ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead)) { + pathEl := d2themes.NewThemableElement("path") + pathEl.D = path + pathEl.Fill = color.None + pathEl.Stroke = connection.Stroke + pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl.Style = connection.CSSStyle() + pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) + fmt.Fprint(writer, pathEl.Render()) + } else { + path1, path2 := splitPath(path, 0.5); - pathEl1 := d2themes.NewThemableElement("path") - pathEl1.D = path1 - pathEl1.Fill = color.None - pathEl1.Stroke = connection.Stroke - pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass) - pathEl1.Style = connection.CSSStyle() - pathEl1.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) - fmt.Fprint(writer, pathEl1.Render()) + pathEl1 := d2themes.NewThemableElement("path") + pathEl1.D = path1 + pathEl1.Fill = color.None + pathEl1.Stroke = connection.Stroke + pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl1.Style = connection.CSSStyle() + pathEl1.Style += "animation-direction: reverse;" + pathEl1.Attributes = fmt.Sprintf("%s%s", markerStart, mask) + fmt.Fprint(writer, pathEl1.Render()) - pathEl2 := d2themes.NewThemableElement("path") - pathEl2.D = path2 - pathEl2.Fill = color.None - pathEl2.Stroke = connection.Stroke - pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass) - pathEl2.Style = connection.CSSStyle() - pathEl2.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) - fmt.Fprint(writer, pathEl2.Render()) + pathEl2 := d2themes.NewThemableElement("path") + pathEl2.D = path2 + pathEl2.Fill = color.None + pathEl2.Stroke = connection.Stroke + pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl2.Style = connection.CSSStyle() + pathEl2.Attributes = fmt.Sprintf("%s%s", markerEnd, mask) + fmt.Fprint(writer, pathEl2.Render()) + } } if connection.Label != "" { diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg index 8bf7169d7..5c7dc4c21 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg @@ -95,7 +95,7 @@ .d2-4137793201 .color-AA4{color:#EDF0FD;} .d2-4137793201 .color-AA5{color:#F7F8FE;} .d2-4137793201 .color-AB4{color:#EDF0FD;} - .d2-4137793201 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefg + .d2-4137793201 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefg diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg index eb9196aad..2beadaab6 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg @@ -95,7 +95,7 @@ .d2-2098060027 .color-AA4{color:#EDF0FD;} .d2-2098060027 .color-AA5{color:#F7F8FE;} .d2-2098060027 .color-AB4{color:#EDF0FD;} - .d2-2098060027 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefg + .d2-2098060027 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefg From a487f954c6d0f9ec4e871f538ecf157d59a1cbfd Mon Sep 17 00:00:00 2001 From: Daniel Suh Date: Tue, 14 May 2024 16:40:20 -0400 Subject: [PATCH 03/12] final commit before refactoring --- d2renderers/d2svg/d2svg.go | 1 + 1 file changed, 1 insertion(+) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 9dc4b7acd..ef2e7bd1a 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -496,6 +496,7 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin } func splitBezierCurve(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { + // Given control points p1, p2, p3, p4, calculate the bezier segment from t0 -> t1 where {0 <= t0 < t1 <= 1} u0, u1 := 1 - t0, 1 - t1 q1 := geo.Point { From 5b9e5ac9c1f525a5417b06563387320f286b4b81 Mon Sep 17 00:00:00 2001 From: Daniel Suh Date: Tue, 14 May 2024 16:43:49 -0400 Subject: [PATCH 04/12] removed prints --- d2renderers/d2svg/d2svg.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index ef2e7bd1a..614c9ed9c 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -562,7 +562,6 @@ func splitPath(path string, percentage float64) (string, string) { prevPosition = geo.Point{X: x, Y: y}; i += increment; } - fmt.Println(pathLength); i := 0 @@ -680,9 +679,6 @@ func splitPath(path string, percentage float64) (string, string) { i += increment } - - fmt.Println(path1); - fmt.Println(path2); return path1, path2 } From 8a38e0d9e91866e2d666b2939ab1476df6903eee Mon Sep 17 00:00:00 2001 From: Daniel Suh Date: Tue, 14 May 2024 17:08:25 -0400 Subject: [PATCH 05/12] format --- d2renderers/d2svg/d2svg.go | 252 +++++++++++++++++++------------------ 1 file changed, 133 insertions(+), 119 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 614c9ed9c..598da23af 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -497,189 +497,203 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin func splitBezierCurve(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { // Given control points p1, p2, p3, p4, calculate the bezier segment from t0 -> t1 where {0 <= t0 < t1 <= 1} - u0, u1 := 1 - t0, 1 - t1 - - q1 := geo.Point { - X: (u0 * u0 * u0) * p1.X + (3 * t0 * u0 * u0) * p2.X + (3 * t0 * t0 * u0) * p3.X + t0 * t0 * t0 * p4.X, - Y: (u0 * u0 * u0) * p1.Y + (3 * t0 * u0 * u0) * p2.Y + (3 * t0 * t0 * u0) * p3.Y + t0 * t0 * t0 * p4.Y, + u0, u1 := 1-t0, 1-t1 + + q1 := geo.Point{ + X: (u0*u0*u0)*p1.X + (3*t0*u0*u0)*p2.X + (3*t0*t0*u0)*p3.X + t0*t0*t0*p4.X, + Y: (u0*u0*u0)*p1.Y + (3*t0*u0*u0)*p2.Y + (3*t0*t0*u0)*p3.Y + t0*t0*t0*p4.Y, } - q2 := geo.Point { - X: (u0 * u0 * u1) * p1.X + (2 * t0 * u0 * u1 + u0 * u0 * t1) * p2.X + (t0 * t0 * u1 + 2 * u0 * t0 * t1) * p3.X + t0 * t0 * t1 * p4.X, - Y: (u0 * u0 * u1) * p1.Y + (2 * t0 * u0 * u1 + u0 * u0 * t1) * p2.Y + (t0 * t0 * u1 + 2 * u0 * t0 * t1) * p3.Y + t0 * t0 * t1 * p4.Y, + q2 := geo.Point{ + X: (u0*u0*u1)*p1.X + (2*t0*u0*u1+u0*u0*t1)*p2.X + (t0*t0*u1+2*u0*t0*t1)*p3.X + t0*t0*t1*p4.X, + Y: (u0*u0*u1)*p1.Y + (2*t0*u0*u1+u0*u0*t1)*p2.Y + (t0*t0*u1+2*u0*t0*t1)*p3.Y + t0*t0*t1*p4.Y, } q3 := geo.Point{ - X: (u0 * u1 * u1) * p1.X + (t0 * u1 * u1 + 2 * u0 * t1 * u1) * p2.X + (2 * t0 * t1 * u1 + u0 * t1 * t1) * p3.X + t0 * t1 * t1 * p4.X, - Y: (u0 * u1 * u1) * p1.Y + (t0 * u1 * u1 + 2 * u0 * t1 * u1) * p2.Y + (2 * t0 * t1 * u1 + u0 * t1 * t1) * p3.Y + t0 * t1 * t1 * p4.Y, + X: (u0*u1*u1)*p1.X + (t0*u1*u1+2*u0*t1*u1)*p2.X + (2*t0*t1*u1+u0*t1*t1)*p3.X + t0*t1*t1*p4.X, + Y: (u0*u1*u1)*p1.Y + (t0*u1*u1+2*u0*t1*u1)*p2.Y + (2*t0*t1*u1+u0*t1*t1)*p3.Y + t0*t1*t1*p4.Y, } q4 := geo.Point{ - X: (u1 * u1 * u1) * p1.X + (3 * t1 * u1 * u1) * p2.X + (3 * t1 * t1 * u1) * p3.X + t1 * t1 * t1 * p4.X, - Y: (u1 * u1 * u1) * p1.Y + (3 * t1 * u1 * u1) * p2.Y + (3 * t1 * t1 * u1) * p3.Y + t1 * t1 * t1 * p4.Y, + X: (u1*u1*u1)*p1.X + (3*t1*u1*u1)*p2.X + (3*t1*t1*u1)*p3.X + t1*t1*t1*p4.X, + Y: (u1*u1*u1)*p1.Y + (3*t1*u1*u1)*p2.Y + (3*t1*t1*u1)*p3.Y + t1*t1*t1*p4.Y, } return q1, q2, q3, q4 } +func addToPath(path *string, pathType *string, i int, pathData []string) int { + var increment int + + switch *pathType { + case "M": + *path += fmt.Sprintf("M %s %s ", pathData[i+1], pathData[i+2]) + increment = 3 + case "L": + *path += fmt.Sprintf("L %s %s ", pathData[i+1], pathData[i+2]) + increment = 3 + case "C": + *path += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4], pathData[i+5], pathData[i+6]) + increment = 7 + case "S": + *path += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) + increment = 5 + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + return increment +} + +func pathLength(pathData []string) float64 { + var x, y, pathLength float64 + var increment int + var prevPosition geo.Point + + for i := 0; i < len(pathData); { + switch pathData[i] { + case "M": + x, _ = strconv.ParseFloat(pathData[i+1], 64) + y, _ = strconv.ParseFloat(pathData[i+2], 64) + + increment = 3 + case "L": + x, _ = strconv.ParseFloat(pathData[i+1], 64) + y, _ = strconv.ParseFloat(pathData[i+2], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 3 + case "C": + x, _ = strconv.ParseFloat(pathData[i+5], 64) + y, _ = strconv.ParseFloat(pathData[i+6], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 7 + case "S": + x, _ = strconv.ParseFloat(pathData[i+3], 64) + y, _ = strconv.ParseFloat(pathData[i+4], 64) + + pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + + increment = 5 + default: + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + } + + prevPosition = geo.Point{X: x, Y: y} + i += increment + } + + return pathLength +} + func splitPath(path string, percentage float64) (string, string) { - var sumPathLens, curPathLen, x, y, pathLength float64 + var sumPathLens, curPathLen, x, y float64 var prevPosition geo.Point var path1, path2 string var increment int pathData := strings.Split(path, " ") - - for i := 0; i < len(pathData); { - switch pathData[i] { - case "M": - x, _ = strconv.ParseFloat(pathData[i + 1], 64) - y, _ = strconv.ParseFloat(pathData[i + 2], 64) - - increment = 3 - case "L": - x, _ = strconv.ParseFloat(pathData[i + 1], 64) - y, _ = strconv.ParseFloat(pathData[i + 2], 64) - - pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - increment = 3 - case "C": - x, _ = strconv.ParseFloat(pathData[i + 5], 64) - y, _ = strconv.ParseFloat(pathData[i + 6], 64) - - pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) + pathLen := pathLength(pathData) - increment = 7 - case "S": - x, _ = strconv.ParseFloat(pathData[i + 3], 64) - y, _ = strconv.ParseFloat(pathData[i + 4], 64) - - pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - increment = 5 - default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) - } - - prevPosition = geo.Point{X: x, Y: y}; - i += increment; - } - i := 0 - - for ; i < len(pathData); { + + for i < len(pathData) { switch pathData[i] { case "M": - x, _ = strconv.ParseFloat(pathData[i + 1], 64) - y, _ = strconv.ParseFloat(pathData[i + 2], 64) + x, _ = strconv.ParseFloat(pathData[i+1], 64) + y, _ = strconv.ParseFloat(pathData[i+2], 64) - if sumPathLens + curPathLen < pathLength * percentage { - path1 += fmt.Sprintf("M %s %s ", pathData[i + 1], pathData[i + 2]) + if sumPathLens+curPathLen < pathLen*percentage { + path1 += fmt.Sprintf("M %s %s ", pathData[i+1], pathData[i+2]) } - + increment = 3 case "L": - x, _ = strconv.ParseFloat(pathData[i + 1], 64) - y, _ = strconv.ParseFloat(pathData[i + 2], 64) - + x, _ = strconv.ParseFloat(pathData[i+1], 64) + y, _ = strconv.ParseFloat(pathData[i+2], 64) + curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - if sumPathLens + curPathLen < pathLength * percentage { - path1 += fmt.Sprintf("L %s %s ", pathData[i + 1], pathData[i + 2]) + if sumPathLens+curPathLen < pathLen*percentage { + path1 += fmt.Sprintf("L %s %s ", pathData[i+1], pathData[i+2]) } - + increment = 3 case "C": - x, _ = strconv.ParseFloat(pathData[i + 5], 64) - y, _ = strconv.ParseFloat(pathData[i + 6], 64) + x, _ = strconv.ParseFloat(pathData[i+5], 64) + y, _ = strconv.ParseFloat(pathData[i+6], 64) curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - if sumPathLens + curPathLen < pathLength * percentage { - path1 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4], pathData[i + 5], pathData[i + 6]); + + if sumPathLens+curPathLen < pathLen*percentage { + path1 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4], pathData[i+5], pathData[i+6]) } increment = 7 case "S": - x, _ = strconv.ParseFloat(pathData[i + 3], 64) - y, _ = strconv.ParseFloat(pathData[i + 4], 64) - + x, _ = strconv.ParseFloat(pathData[i+3], 64) + y, _ = strconv.ParseFloat(pathData[i+4], 64) + curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - if sumPathLens + curPathLen < pathLength * percentage { - path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) + + if sumPathLens+curPathLen < pathLen*percentage { + path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) } - + increment = 5 default: panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) } - + sumPathLens += curPathLen - if sumPathLens >= pathLength * percentage { - t := (pathLength * percentage - sumPathLens + curPathLen) / curPathLen - fmt.Println(t) + if sumPathLens >= pathLen*percentage { + t := (pathLen*percentage - sumPathLens + curPathLen) / curPathLen - switch(pathData[i]) { + switch pathData[i] { case "L": - path1 += fmt.Sprintf("L %f %f ", (x - prevPosition.X) * t + prevPosition.X, (y - prevPosition.Y) * t + prevPosition.Y) - path2 += fmt.Sprintf("M %f %f L %f %f ", (x - prevPosition.X) * t + prevPosition.X, (y - prevPosition.Y) * t + prevPosition.Y, x, y) + path1 += fmt.Sprintf("L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y) + path2 += fmt.Sprintf("M %f %f L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y, x, y) case "C": - h1x, _ := strconv.ParseFloat(pathData[i + 1], 64) - h1y, _ := strconv.ParseFloat(pathData[i + 2], 64) - h2x, _ := strconv.ParseFloat(pathData[i + 3], 64) - h2y, _ := strconv.ParseFloat(pathData[i + 4], 64) - p1x, _ := strconv.ParseFloat(pathData[i + 5], 64) - p1y, _ := strconv.ParseFloat(pathData[i + 6], 64) + h1x, _ := strconv.ParseFloat(pathData[i+1], 64) + h1y, _ := strconv.ParseFloat(pathData[i+2], 64) + h2x, _ := strconv.ParseFloat(pathData[i+3], 64) + h2y, _ := strconv.ParseFloat(pathData[i+4], 64) heading1 := geo.Point{X: h1x, Y: h1y} heading2 := geo.Point{X: h2x, Y: h2y} - nextPoint := geo.Point{X: p1x, Y: p1y} - + nextPoint := geo.Point{X: x, Y: y} + q1, q2, q3, q4 := splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) - - path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y); - + path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) + q1, q2, q3, q4 = splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) - - path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y); - + path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) + case "S": - path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) - path2 += fmt.Sprintf("M %s %s ", pathData[i + 3], pathData[i + 4]) + // Skip S curves because they are shorter and we can split along the connection to the next path instead + path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) + path2 += fmt.Sprintf("M %s %s ", pathData[i+3], pathData[i+4]) default: panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) } - + i += increment prevPosition = geo.Point{X: x, Y: y} break - } + } i += increment prevPosition = geo.Point{X: x, Y: y} } - for ; i < len(pathData); { - switch pathData[i] { - case "M": - path2 += fmt.Sprintf("M %s %s ", pathData[i + 1], pathData[i + 2]) - increment = 3 - case "L": - path2 += fmt.Sprintf("L %s %s ", pathData[i + 1], pathData[i + 2]) - increment = 3 - case "C": - path2 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4], pathData[i + 5], pathData[i + 6]); - increment = 7 - case "S": - path2 += fmt.Sprintf("S %s %s %s %s ", pathData[i + 1], pathData[i + 2], pathData[i + 3], pathData[i + 4]) - increment = 5 - default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) - } + for i < len(pathData) { + pathType := pathData[i] + increment := addToPath(&path2, &pathType, i, pathData) i += increment } - + return path1, path2 } @@ -738,7 +752,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape) path := pathData(connection, srcAdj, dstAdj) mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID) - + if sketchRunner != nil { out, err := d2sketch.Connection(sketchRunner, connection, path, mask) if err != nil { @@ -769,7 +783,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) fmt.Fprint(writer, pathEl.Render()) } else { - path1, path2 := splitPath(path, 0.5); + path1, path2 := splitPath(path, 0.5) pathEl1 := d2themes.NewThemableElement("path") pathEl1.D = path1 From 49df5b81c07bfa57d824b471eaa493ec18b2252a Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Tue, 14 May 2024 18:21:44 -0400 Subject: [PATCH 06/12] shorten code --- d2renderers/d2svg/d2svg.go | 56 ++++++++++++-------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 598da23af..594569ddc 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -495,7 +495,7 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin ) } -func splitBezierCurve(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { +func bezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { // Given control points p1, p2, p3, p4, calculate the bezier segment from t0 -> t1 where {0 <= t0 < t1 <= 1} u0, u1 := 1-t0, 1-t1 @@ -591,6 +591,7 @@ func splitPath(path string, percentage float64) (string, string) { var prevPosition geo.Point var path1, path2 string var increment int + var pastHalf bool = false pathData := strings.Split(path, " ") pathLen := pathLength(pathData) @@ -598,62 +599,47 @@ func splitPath(path string, percentage float64) (string, string) { i := 0 for i < len(pathData) { + switch pathData[i] { case "M": x, _ = strconv.ParseFloat(pathData[i+1], 64) y, _ = strconv.ParseFloat(pathData[i+2], 64) - - if sumPathLens+curPathLen < pathLen*percentage { - path1 += fmt.Sprintf("M %s %s ", pathData[i+1], pathData[i+2]) - } - - increment = 3 case "L": x, _ = strconv.ParseFloat(pathData[i+1], 64) y, _ = strconv.ParseFloat(pathData[i+2], 64) curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - if sumPathLens+curPathLen < pathLen*percentage { - path1 += fmt.Sprintf("L %s %s ", pathData[i+1], pathData[i+2]) - } - - increment = 3 case "C": x, _ = strconv.ParseFloat(pathData[i+5], 64) y, _ = strconv.ParseFloat(pathData[i+6], 64) curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - if sumPathLens+curPathLen < pathLen*percentage { - path1 += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4], pathData[i+5], pathData[i+6]) - } - - increment = 7 case "S": x, _ = strconv.ParseFloat(pathData[i+3], 64) y, _ = strconv.ParseFloat(pathData[i+4], 64) curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - if sumPathLens+curPathLen < pathLen*percentage { - path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) - } - - increment = 5 default: panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) } + if pastHalf { + increment = addToPath(&path2, &pathData[i], i, pathData) + } else if sumPathLens+curPathLen < pathLen*percentage { + increment = addToPath(&path1, &pathData[i], i, pathData) + } + sumPathLens += curPathLen - if sumPathLens >= pathLen*percentage { + if !pastHalf && sumPathLens >= pathLen*percentage { t := (pathLen*percentage - sumPathLens + curPathLen) / curPathLen switch pathData[i] { case "L": path1 += fmt.Sprintf("L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y) path2 += fmt.Sprintf("M %f %f L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y, x, y) + + increment = 3 case "C": h1x, _ := strconv.ParseFloat(pathData[i+1], 64) h1y, _ := strconv.ParseFloat(pathData[i+2], 64) @@ -664,36 +650,30 @@ func splitPath(path string, percentage float64) (string, string) { heading2 := geo.Point{X: h2x, Y: h2y} nextPoint := geo.Point{X: x, Y: y} - q1, q2, q3, q4 := splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) + q1, q2, q3, q4 := bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) - q1, q2, q3, q4 = splitBezierCurve(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) + q1, q2, q3, q4 = bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) + increment = 7 case "S": // Skip S curves because they are shorter and we can split along the connection to the next path instead path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) path2 += fmt.Sprintf("M %s %s ", pathData[i+3], pathData[i+4]) + + increment = 5 default: panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) } - i += increment - prevPosition = geo.Point{X: x, Y: y} - break + pastHalf = true } i += increment prevPosition = geo.Point{X: x, Y: y} } - for i < len(pathData) { - pathType := pathData[i] - increment := addToPath(&path2, &pathType, i, pathData) - - i += increment - } - return path1, path2 } From 97975425e19d6839b5237e917b18c11ee6b4bd10 Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Tue, 14 May 2024 18:44:43 -0400 Subject: [PATCH 07/12] final changes --- d2renderers/d2svg/d2svg.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 594569ddc..9b2484433 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -495,8 +495,8 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin ) } +// Given control points p1, p2, p3, p4, calculate the segment of this bezier curve from t0 -> t1 where {0 <= t0 < t1 <= 1} func bezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { - // Given control points p1, p2, p3, p4, calculate the bezier segment from t0 -> t1 where {0 <= t0 < t1 <= 1} u0, u1 := 1-t0, 1-t1 q1 := geo.Point{ @@ -519,29 +519,31 @@ func bezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, g return q1, q2, q3, q4 } -func addToPath(path *string, pathType *string, i int, pathData []string) int { +// Adds a certain line/curve to an existing SVG path string. offsetIdx and pathData provides the points needed +func addToPath(path *string, pathType *string, offsetIdx int, pathData []string) int { var increment int switch *pathType { case "M": - *path += fmt.Sprintf("M %s %s ", pathData[i+1], pathData[i+2]) + *path += fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) increment = 3 case "L": - *path += fmt.Sprintf("L %s %s ", pathData[i+1], pathData[i+2]) + *path += fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) increment = 3 case "C": - *path += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4], pathData[i+5], pathData[i+6]) + *path += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]) increment = 7 case "S": - *path += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) + *path += fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]) increment = 5 default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[offsetIdx])) } return increment } +// This function finds the length of a path in SVG notation func pathLength(pathData []string) float64 { var x, y, pathLength float64 var increment int @@ -586,13 +588,14 @@ func pathLength(pathData []string) float64 { return pathLength } +// Splits a SVG path into two SVG paths, with the first path being ~{percentage}% of the path func splitPath(path string, percentage float64) (string, string) { var sumPathLens, curPathLen, x, y float64 var prevPosition geo.Point var path1, path2 string var increment int - var pastHalf bool = false + pastHalf := false pathData := strings.Split(path, " ") pathLen := pathLength(pathData) From 4abe7fb71cb8173abbe3f75e56476efa376f564a Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Tue, 14 May 2024 19:50:32 -0400 Subject: [PATCH 08/12] passes tests Signed-off-by: Daniel Suh <23denial@gmail.com> --- e2etests/testdata/stable/animated/dagre/sketch.exp.svg | 2 +- e2etests/testdata/stable/animated/elk/sketch.exp.svg | 2 +- .../stable/sql_table_tooltip_animated/dagre/sketch.exp.svg | 2 +- .../stable/sql_table_tooltip_animated/elk/sketch.exp.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg index dc10385f0..94ca46c77 100644 --- a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg @@ -102,7 +102,7 @@ .d2-3267239171 .color-AA4{color:#EDF0FD;} .d2-3267239171 .color-AA5{color:#F7F8FE;} .d2-3267239171 .color-AB4{color:#EDF0FD;} - .d2-3267239171 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear + .d2-3267239171 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear diff --git a/e2etests/testdata/stable/animated/elk/sketch.exp.svg b/e2etests/testdata/stable/animated/elk/sketch.exp.svg index 02e88aef7..bffee0710 100644 --- a/e2etests/testdata/stable/animated/elk/sketch.exp.svg +++ b/e2etests/testdata/stable/animated/elk/sketch.exp.svg @@ -102,7 +102,7 @@ .d2-838869033 .color-AA4{color:#EDF0FD;} .d2-838869033 .color-AA5{color:#F7F8FE;} .d2-838869033 .color-AB4{color:#EDF0FD;} - .d2-838869033 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear + .d2-838869033 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg index 0cd9b646b..a6b122b6a 100644 --- a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg @@ -98,7 +98,7 @@ .d2-3096218097 .color-AA4{color:#EDF0FD;} .d2-3096218097 .color-AA5{color:#F7F8FE;} .d2-3096218097 .color-AB4{color:#EDF0FD;} - .d2-3096218097 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles + .d2-3096218097 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles 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 index 4acd2e6e4..698aafa98 100644 --- a/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg +++ b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg @@ -98,7 +98,7 @@ .d2-3579465052 .color-AA4{color:#EDF0FD;} .d2-3579465052 .color-AA5{color:#F7F8FE;} .d2-3579465052 .color-AB4{color:#EDF0FD;} - .d2-3579465052 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles + .d2-3579465052 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles From c2347859a28b22e68759eeb495663344778c9442 Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Thu, 16 May 2024 23:14:15 -0400 Subject: [PATCH 09/12] fixed requested changes --- d2renderers/d2svg/d2svg.go | 119 ++++++------- e2etests/testdata/txtar.txt | 1 + .../dagre/board.exp.json | 124 ++++++++++++++ .../dagre/sketch.exp.svg | 157 +++++++++--------- .../elk/board.exp.json | 87 ++++++++++ .../elk/sketch.exp.svg | 157 +++++++++--------- lib/svg/path.go | 25 +++ 7 files changed, 458 insertions(+), 212 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 15253b895..918b6c2bf 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -495,58 +495,49 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin ) } -// Given control points p1, p2, p3, p4, calculate the segment of this bezier curve from t0 -> t1 where {0 <= t0 < t1 <= 1} -func bezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { - u0, u1 := 1-t0, 1-t1 +// Gets a certain line/curve's SVG path string. offsetIdx and pathData provides the points needed +func getSVGPathString(path *string, pathType *string, offsetIdx int, pathData []string) (string, error) { + var returnPath string - q1 := geo.Point{ - X: (u0*u0*u0)*p1.X + (3*t0*u0*u0)*p2.X + (3*t0*t0*u0)*p3.X + t0*t0*t0*p4.X, - Y: (u0*u0*u0)*p1.Y + (3*t0*u0*u0)*p2.Y + (3*t0*t0*u0)*p3.Y + t0*t0*t0*p4.Y, - } - q2 := geo.Point{ - X: (u0*u0*u1)*p1.X + (2*t0*u0*u1+u0*u0*t1)*p2.X + (t0*t0*u1+2*u0*t0*t1)*p3.X + t0*t0*t1*p4.X, - Y: (u0*u0*u1)*p1.Y + (2*t0*u0*u1+u0*u0*t1)*p2.Y + (t0*t0*u1+2*u0*t0*t1)*p3.Y + t0*t0*t1*p4.Y, - } - q3 := geo.Point{ - X: (u0*u1*u1)*p1.X + (t0*u1*u1+2*u0*t1*u1)*p2.X + (2*t0*t1*u1+u0*t1*t1)*p3.X + t0*t1*t1*p4.X, - Y: (u0*u1*u1)*p1.Y + (t0*u1*u1+2*u0*t1*u1)*p2.Y + (2*t0*t1*u1+u0*t1*t1)*p3.Y + t0*t1*t1*p4.Y, - } - q4 := geo.Point{ - X: (u1*u1*u1)*p1.X + (3*t1*u1*u1)*p2.X + (3*t1*t1*u1)*p3.X + t1*t1*t1*p4.X, - Y: (u1*u1*u1)*p1.Y + (3*t1*u1*u1)*p2.Y + (3*t1*t1*u1)*p3.Y + t1*t1*t1*p4.Y, + switch *pathType { + case "M": + returnPath = fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) + case "L": + returnPath = fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) + case "C": + returnPath = fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]) + case "S": + returnPath = fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]) + default: + return "", fmt.Errorf("unknown svg path command \"%s\"", pathData[offsetIdx]) } - return q1, q2, q3, q4 + return returnPath, nil } -// Adds a certain line/curve to an existing SVG path string. offsetIdx and pathData provides the points needed -func addToPath(path *string, pathType *string, offsetIdx int, pathData []string) int { +// Gets how much to increment by on an SVG string to get to the next path command +func getPathStringIncrement(pathType *string) (int, error) { var increment int switch *pathType { case "M": - *path += fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) increment = 3 case "L": - *path += fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) increment = 3 case "C": - *path += fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]) increment = 7 case "S": - *path += fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]) increment = 5 default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[offsetIdx])) + return 0, fmt.Errorf("unknown svg path command \"%s\"", *pathType) } - return increment + return increment, nil } // This function finds the length of a path in SVG notation -func pathLength(pathData []string) float64 { +func pathLength(pathData []string) (float64, error) { var x, y, pathLength float64 - var increment int var prevPosition geo.Point for i := 0; i < len(pathData); { @@ -554,50 +545,51 @@ func pathLength(pathData []string) float64 { case "M": x, _ = strconv.ParseFloat(pathData[i+1], 64) y, _ = strconv.ParseFloat(pathData[i+2], 64) - - increment = 3 case "L": x, _ = strconv.ParseFloat(pathData[i+1], 64) y, _ = strconv.ParseFloat(pathData[i+2], 64) pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - increment = 3 case "C": x, _ = strconv.ParseFloat(pathData[i+5], 64) y, _ = strconv.ParseFloat(pathData[i+6], 64) pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - increment = 7 case "S": x, _ = strconv.ParseFloat(pathData[i+3], 64) y, _ = strconv.ParseFloat(pathData[i+4], 64) pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) - - increment = 5 default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + return 0, fmt.Errorf("unknown svg path command \"%s\"", pathData[i]) } prevPosition = geo.Point{X: x, Y: y} + increment, err := getPathStringIncrement(&pathData[i]) + + if err != nil { + return 0, err + } + i += increment } - return pathLength + return pathLength, nil } // Splits a SVG path into two SVG paths, with the first path being ~{percentage}% of the path -func splitPath(path string, percentage float64) (string, string) { +func splitPath(path string, percentage float64) (string, string, error) { var sumPathLens, curPathLen, x, y float64 var prevPosition geo.Point var path1, path2 string - var increment int pastHalf := false pathData := strings.Split(path, " ") - pathLen := pathLength(pathData) + pathLen, err := pathLength(pathData) + + if err != nil { + return "", "", err + } i := 0 @@ -623,13 +615,24 @@ func splitPath(path string, percentage float64) (string, string) { curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y) default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i]) } if pastHalf { - increment = addToPath(&path2, &pathData[i], i, pathData) + curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData) + path2 += curPath + + if err != nil { + return "", "", err + } + } else if sumPathLens+curPathLen < pathLen*percentage { - increment = addToPath(&path1, &pathData[i], i, pathData) + curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData) + path1 += curPath + + if err != nil { + return "", "", err + } } sumPathLens += curPathLen @@ -641,8 +644,6 @@ func splitPath(path string, percentage float64) (string, string) { case "L": path1 += fmt.Sprintf("L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y) path2 += fmt.Sprintf("M %f %f L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y, x, y) - - increment = 3 case "C": h1x, _ := strconv.ParseFloat(pathData[i+1], 64) h1y, _ := strconv.ParseFloat(pathData[i+2], 64) @@ -653,31 +654,33 @@ func splitPath(path string, percentage float64) (string, string) { heading2 := geo.Point{X: h2x, Y: h2y} nextPoint := geo.Point{X: x, Y: y} - q1, q2, q3, q4 := bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) + q1, q2, q3, q4 := svg.BezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5) path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) - q1, q2, q3, q4 = bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) + q1, q2, q3, q4 = svg.BezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1) path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y) - - increment = 7 case "S": // Skip S curves because they are shorter and we can split along the connection to the next path instead path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4]) path2 += fmt.Sprintf("M %s %s ", pathData[i+3], pathData[i+4]) - - increment = 5 default: - panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i])) + return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i]) } pastHalf = true } + increment, err := getPathStringIncrement(&pathData[i]) + + if err != nil { + return "", "", err + } + i += increment prevPosition = geo.Point{X: x, Y: y} } - return path1, path2 + return path1, path2, nil } func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) { @@ -766,7 +769,11 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) fmt.Fprint(writer, pathEl.Render()) } else { - path1, path2 := splitPath(path, 0.5) + path1, path2, err := splitPath(path, 0.5) + + if err != nil { + return "", err + } pathEl1 := d2themes.NewThemableElement("path") pathEl1.D = path1 diff --git a/e2etests/testdata/txtar.txt b/e2etests/testdata/txtar.txt index b5b0bece6..706a29a47 100644 --- a/e2etests/testdata/txtar.txt +++ b/e2etests/testdata/txtar.txt @@ -220,6 +220,7 @@ a <-> c: {style.animated: true} a <-> d: {style.animated: true} a <-> e: {style.animated: true} f <-> g: {style.animated: true} +x -- x: {style.animated: true} -- opacity-zero-route -- grid: { diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json index 1c4c39898..16c3d88b9 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json @@ -289,6 +289,47 @@ "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 + }, + { + "id": "x", + "type": "rectangle", + "pos": { + "x": 566, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 } ], "connections": [ @@ -526,6 +567,89 @@ "tooltip": "", "icon": null, "zIndex": 0 + }, + { + "id": "(x -- x)[0]", + "src": "x", + "srcArrow": "none", + "dst": "x", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 619.166015625, + "y": 16 + }, + { + "x": 639.9660034179688, + "y": 3.1989998817443848 + }, + { + "x": 646.5, + "y": 0 + }, + { + "x": 648.5, + "y": 0 + }, + { + "x": 650.5, + "y": 0 + }, + { + "x": 653.166015625, + "y": 6.599999904632568 + }, + { + "x": 655.166015625, + "y": 16.5 + }, + { + "x": 657.166015625, + "y": 26.399999618530273 + }, + { + "x": 657.166015625, + "y": 39.599998474121094 + }, + { + "x": 655.166015625, + "y": 49.5 + }, + { + "x": 653.166015625, + "y": 59.400001525878906 + }, + { + "x": 639.9660034179688, + "y": 62.79999923706055 + }, + { + "x": 619.166015625, + "y": 50 + } + ], + "isCurve": true, + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 } ], "root": { diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg index 5c7dc4c21..030120280 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg @@ -1,16 +1,16 @@ -abcdefg - + .d2-2972989750 .fill-N1{fill:#0A0F25;} + .d2-2972989750 .fill-N2{fill:#676C7E;} + .d2-2972989750 .fill-N3{fill:#9499AB;} + .d2-2972989750 .fill-N4{fill:#CFD2DD;} + .d2-2972989750 .fill-N5{fill:#DEE1EB;} + .d2-2972989750 .fill-N6{fill:#EEF1F8;} + .d2-2972989750 .fill-N7{fill:#FFFFFF;} + .d2-2972989750 .fill-B1{fill:#0D32B2;} + .d2-2972989750 .fill-B2{fill:#0D32B2;} + .d2-2972989750 .fill-B3{fill:#E3E9FD;} + .d2-2972989750 .fill-B4{fill:#E3E9FD;} + .d2-2972989750 .fill-B5{fill:#EDF0FD;} + .d2-2972989750 .fill-B6{fill:#F7F8FE;} + .d2-2972989750 .fill-AA2{fill:#4A6FF3;} + .d2-2972989750 .fill-AA4{fill:#EDF0FD;} + .d2-2972989750 .fill-AA5{fill:#F7F8FE;} + .d2-2972989750 .fill-AB4{fill:#EDF0FD;} + .d2-2972989750 .fill-AB5{fill:#F7F8FE;} + .d2-2972989750 .stroke-N1{stroke:#0A0F25;} + .d2-2972989750 .stroke-N2{stroke:#676C7E;} + .d2-2972989750 .stroke-N3{stroke:#9499AB;} + .d2-2972989750 .stroke-N4{stroke:#CFD2DD;} + .d2-2972989750 .stroke-N5{stroke:#DEE1EB;} + .d2-2972989750 .stroke-N6{stroke:#EEF1F8;} + .d2-2972989750 .stroke-N7{stroke:#FFFFFF;} + .d2-2972989750 .stroke-B1{stroke:#0D32B2;} + .d2-2972989750 .stroke-B2{stroke:#0D32B2;} + .d2-2972989750 .stroke-B3{stroke:#E3E9FD;} + .d2-2972989750 .stroke-B4{stroke:#E3E9FD;} + .d2-2972989750 .stroke-B5{stroke:#EDF0FD;} + .d2-2972989750 .stroke-B6{stroke:#F7F8FE;} + .d2-2972989750 .stroke-AA2{stroke:#4A6FF3;} + .d2-2972989750 .stroke-AA4{stroke:#EDF0FD;} + .d2-2972989750 .stroke-AA5{stroke:#F7F8FE;} + .d2-2972989750 .stroke-AB4{stroke:#EDF0FD;} + .d2-2972989750 .stroke-AB5{stroke:#F7F8FE;} + .d2-2972989750 .background-color-N1{background-color:#0A0F25;} + .d2-2972989750 .background-color-N2{background-color:#676C7E;} + .d2-2972989750 .background-color-N3{background-color:#9499AB;} + .d2-2972989750 .background-color-N4{background-color:#CFD2DD;} + .d2-2972989750 .background-color-N5{background-color:#DEE1EB;} + .d2-2972989750 .background-color-N6{background-color:#EEF1F8;} + .d2-2972989750 .background-color-N7{background-color:#FFFFFF;} + .d2-2972989750 .background-color-B1{background-color:#0D32B2;} + .d2-2972989750 .background-color-B2{background-color:#0D32B2;} + .d2-2972989750 .background-color-B3{background-color:#E3E9FD;} + .d2-2972989750 .background-color-B4{background-color:#E3E9FD;} + .d2-2972989750 .background-color-B5{background-color:#EDF0FD;} + .d2-2972989750 .background-color-B6{background-color:#F7F8FE;} + .d2-2972989750 .background-color-AA2{background-color:#4A6FF3;} + .d2-2972989750 .background-color-AA4{background-color:#EDF0FD;} + .d2-2972989750 .background-color-AA5{background-color:#F7F8FE;} + .d2-2972989750 .background-color-AB4{background-color:#EDF0FD;} + .d2-2972989750 .background-color-AB5{background-color:#F7F8FE;} + .d2-2972989750 .color-N1{color:#0A0F25;} + .d2-2972989750 .color-N2{color:#676C7E;} + .d2-2972989750 .color-N3{color:#9499AB;} + .d2-2972989750 .color-N4{color:#CFD2DD;} + .d2-2972989750 .color-N5{color:#DEE1EB;} + .d2-2972989750 .color-N6{color:#EEF1F8;} + .d2-2972989750 .color-N7{color:#FFFFFF;} + .d2-2972989750 .color-B1{color:#0D32B2;} + .d2-2972989750 .color-B2{color:#0D32B2;} + .d2-2972989750 .color-B3{color:#E3E9FD;} + .d2-2972989750 .color-B4{color:#E3E9FD;} + .d2-2972989750 .color-B5{color:#EDF0FD;} + .d2-2972989750 .color-B6{color:#F7F8FE;} + .d2-2972989750 .color-AA2{color:#4A6FF3;} + .d2-2972989750 .color-AA4{color:#EDF0FD;} + .d2-2972989750 .color-AA5{color:#F7F8FE;} + .d2-2972989750 .color-AB4{color:#EDF0FD;} + .d2-2972989750 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefgx + @@ -104,4 +104,5 @@ + \ No newline at end of file diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json index e121ae7ac..85d12fc27 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json @@ -289,6 +289,47 @@ "labelPosition": "INSIDE_MIDDLE_CENTER", "zIndex": 0, "level": 1 + }, + { + "id": "x", + "type": "rectangle", + "pos": { + "x": 427, + "y": 12 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 } ], "connections": [ @@ -513,6 +554,52 @@ "tooltip": "", "icon": null, "zIndex": 0 + }, + { + "id": "(x -- x)[0]", + "src": "x", + "srcArrow": "none", + "dst": "x", + "dstArrow": "none", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 427.5, + "y": 34 + }, + { + "x": 377.5, + "y": 34 + }, + { + "x": 377.5, + "y": 56 + }, + { + "x": 427.5, + "y": 56 + } + ], + "animated": true, + "tooltip": "", + "icon": null, + "zIndex": 0 } ], "root": { diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg index 2beadaab6..5bdfdd2c5 100644 --- a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg +++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg @@ -1,16 +1,16 @@ -abcdefg - + .d2-2574831872 .fill-N1{fill:#0A0F25;} + .d2-2574831872 .fill-N2{fill:#676C7E;} + .d2-2574831872 .fill-N3{fill:#9499AB;} + .d2-2574831872 .fill-N4{fill:#CFD2DD;} + .d2-2574831872 .fill-N5{fill:#DEE1EB;} + .d2-2574831872 .fill-N6{fill:#EEF1F8;} + .d2-2574831872 .fill-N7{fill:#FFFFFF;} + .d2-2574831872 .fill-B1{fill:#0D32B2;} + .d2-2574831872 .fill-B2{fill:#0D32B2;} + .d2-2574831872 .fill-B3{fill:#E3E9FD;} + .d2-2574831872 .fill-B4{fill:#E3E9FD;} + .d2-2574831872 .fill-B5{fill:#EDF0FD;} + .d2-2574831872 .fill-B6{fill:#F7F8FE;} + .d2-2574831872 .fill-AA2{fill:#4A6FF3;} + .d2-2574831872 .fill-AA4{fill:#EDF0FD;} + .d2-2574831872 .fill-AA5{fill:#F7F8FE;} + .d2-2574831872 .fill-AB4{fill:#EDF0FD;} + .d2-2574831872 .fill-AB5{fill:#F7F8FE;} + .d2-2574831872 .stroke-N1{stroke:#0A0F25;} + .d2-2574831872 .stroke-N2{stroke:#676C7E;} + .d2-2574831872 .stroke-N3{stroke:#9499AB;} + .d2-2574831872 .stroke-N4{stroke:#CFD2DD;} + .d2-2574831872 .stroke-N5{stroke:#DEE1EB;} + .d2-2574831872 .stroke-N6{stroke:#EEF1F8;} + .d2-2574831872 .stroke-N7{stroke:#FFFFFF;} + .d2-2574831872 .stroke-B1{stroke:#0D32B2;} + .d2-2574831872 .stroke-B2{stroke:#0D32B2;} + .d2-2574831872 .stroke-B3{stroke:#E3E9FD;} + .d2-2574831872 .stroke-B4{stroke:#E3E9FD;} + .d2-2574831872 .stroke-B5{stroke:#EDF0FD;} + .d2-2574831872 .stroke-B6{stroke:#F7F8FE;} + .d2-2574831872 .stroke-AA2{stroke:#4A6FF3;} + .d2-2574831872 .stroke-AA4{stroke:#EDF0FD;} + .d2-2574831872 .stroke-AA5{stroke:#F7F8FE;} + .d2-2574831872 .stroke-AB4{stroke:#EDF0FD;} + .d2-2574831872 .stroke-AB5{stroke:#F7F8FE;} + .d2-2574831872 .background-color-N1{background-color:#0A0F25;} + .d2-2574831872 .background-color-N2{background-color:#676C7E;} + .d2-2574831872 .background-color-N3{background-color:#9499AB;} + .d2-2574831872 .background-color-N4{background-color:#CFD2DD;} + .d2-2574831872 .background-color-N5{background-color:#DEE1EB;} + .d2-2574831872 .background-color-N6{background-color:#EEF1F8;} + .d2-2574831872 .background-color-N7{background-color:#FFFFFF;} + .d2-2574831872 .background-color-B1{background-color:#0D32B2;} + .d2-2574831872 .background-color-B2{background-color:#0D32B2;} + .d2-2574831872 .background-color-B3{background-color:#E3E9FD;} + .d2-2574831872 .background-color-B4{background-color:#E3E9FD;} + .d2-2574831872 .background-color-B5{background-color:#EDF0FD;} + .d2-2574831872 .background-color-B6{background-color:#F7F8FE;} + .d2-2574831872 .background-color-AA2{background-color:#4A6FF3;} + .d2-2574831872 .background-color-AA4{background-color:#EDF0FD;} + .d2-2574831872 .background-color-AA5{background-color:#F7F8FE;} + .d2-2574831872 .background-color-AB4{background-color:#EDF0FD;} + .d2-2574831872 .background-color-AB5{background-color:#F7F8FE;} + .d2-2574831872 .color-N1{color:#0A0F25;} + .d2-2574831872 .color-N2{color:#676C7E;} + .d2-2574831872 .color-N3{color:#9499AB;} + .d2-2574831872 .color-N4{color:#CFD2DD;} + .d2-2574831872 .color-N5{color:#DEE1EB;} + .d2-2574831872 .color-N6{color:#EEF1F8;} + .d2-2574831872 .color-N7{color:#FFFFFF;} + .d2-2574831872 .color-B1{color:#0D32B2;} + .d2-2574831872 .color-B2{color:#0D32B2;} + .d2-2574831872 .color-B3{color:#E3E9FD;} + .d2-2574831872 .color-B4{color:#E3E9FD;} + .d2-2574831872 .color-B5{color:#EDF0FD;} + .d2-2574831872 .color-B6{color:#F7F8FE;} + .d2-2574831872 .color-AA2{color:#4A6FF3;} + .d2-2574831872 .color-AA4{color:#EDF0FD;} + .d2-2574831872 .color-AA5{color:#F7F8FE;} + .d2-2574831872 .color-AB4{color:#EDF0FD;} + .d2-2574831872 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcdefgx + @@ -104,4 +104,5 @@ + \ No newline at end of file diff --git a/lib/svg/path.go b/lib/svg/path.go index cc3ac8ef2..eddbb970f 100644 --- a/lib/svg/path.go +++ b/lib/svg/path.go @@ -114,3 +114,28 @@ func GetStrokeDashAttributes(strokeWidth, dashGapSize float64) (float64, float64 scaledGapSize := scale * scaledDashSize return scaledDashSize, scaledGapSize } + +// Given control points p1, p2, p3, p4, calculate the segment of this bezier curve from t0 -> t1 where {0 <= t0 < t1 <= 1}. +// Uses De Casteljau's algorithm, referenced: https://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment/11704152#11704152 +func BezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) { + u0, u1 := 1-t0, 1-t1 + + q1 := geo.Point{ + X: (u0*u0*u0)*p1.X + (3*t0*u0*u0)*p2.X + (3*t0*t0*u0)*p3.X + t0*t0*t0*p4.X, + Y: (u0*u0*u0)*p1.Y + (3*t0*u0*u0)*p2.Y + (3*t0*t0*u0)*p3.Y + t0*t0*t0*p4.Y, + } + q2 := geo.Point{ + X: (u0*u0*u1)*p1.X + (2*t0*u0*u1+u0*u0*t1)*p2.X + (t0*t0*u1+2*u0*t0*t1)*p3.X + t0*t0*t1*p4.X, + Y: (u0*u0*u1)*p1.Y + (2*t0*u0*u1+u0*u0*t1)*p2.Y + (t0*t0*u1+2*u0*t0*t1)*p3.Y + t0*t0*t1*p4.Y, + } + q3 := geo.Point{ + X: (u0*u1*u1)*p1.X + (t0*u1*u1+2*u0*t1*u1)*p2.X + (2*t0*t1*u1+u0*t1*t1)*p3.X + t0*t1*t1*p4.X, + Y: (u0*u1*u1)*p1.Y + (t0*u1*u1+2*u0*t1*u1)*p2.Y + (2*t0*t1*u1+u0*t1*t1)*p3.Y + t0*t1*t1*p4.Y, + } + q4 := geo.Point{ + X: (u1*u1*u1)*p1.X + (3*t1*u1*u1)*p2.X + (3*t1*t1*u1)*p3.X + t1*t1*t1*p4.X, + Y: (u1*u1*u1)*p1.Y + (3*t1*u1*u1)*p2.Y + (3*t1*t1*u1)*p3.Y + t1*t1*t1*p4.Y, + } + + return q1, q2, q3, q4 +} From 99f5596e8d7064bf36f106cfb1a1e4d64396d396 Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Thu, 16 May 2024 23:41:50 -0400 Subject: [PATCH 10/12] shortened for loops --- ci/release/changelogs/next.md | 1 + d2renderers/d2svg/d2svg.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 10845d9dd..f0d6d64d8 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -3,6 +3,7 @@ #### Improvements 🧹 - Opacity 0 shapes no longer have a label mask which made any segment of connections going through them lower opacity [#1940](https://github.com/terrastruct/d2/pull/1940) +- Bidirectional connections are now animated in opposite directions rather than one direction [#1939](https://github.com/terrastruct/d2/pull/1939) #### Bugfixes ⛑️ diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 918b6c2bf..2a500574b 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -539,8 +539,9 @@ func getPathStringIncrement(pathType *string) (int, error) { func pathLength(pathData []string) (float64, error) { var x, y, pathLength float64 var prevPosition geo.Point + var increment int - for i := 0; i < len(pathData); { + for i := 0; i < len(pathData); i += increment { switch pathData[i] { case "M": x, _ = strconv.ParseFloat(pathData[i+1], 64) @@ -565,13 +566,13 @@ func pathLength(pathData []string) (float64, error) { } prevPosition = geo.Point{X: x, Y: y} - increment, err := getPathStringIncrement(&pathData[i]) + + incr, err := getPathStringIncrement(&pathData[i]) + increment = incr if err != nil { return 0, err } - - i += increment } return pathLength, nil @@ -582,6 +583,7 @@ func splitPath(path string, percentage float64) (string, string, error) { var sumPathLens, curPathLen, x, y float64 var prevPosition geo.Point var path1, path2 string + var increment int pastHalf := false pathData := strings.Split(path, " ") @@ -591,9 +593,7 @@ func splitPath(path string, percentage float64) (string, string, error) { return "", "", err } - i := 0 - - for i < len(pathData) { + for i := 0; i < len(pathData); i += increment { switch pathData[i] { case "M": @@ -670,13 +670,13 @@ func splitPath(path string, percentage float64) (string, string, error) { pastHalf = true } - increment, err := getPathStringIncrement(&pathData[i]) + incr, err := getPathStringIncrement(&pathData[i]) + increment = incr if err != nil { return "", "", err } - i += increment prevPosition = geo.Point{X: x, Y: y} } From f8003a0775376fb543db24f3ccda043b0a92f7ad Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Sat, 18 May 2024 12:52:04 -0400 Subject: [PATCH 11/12] refactor --- d2renderers/d2svg/d2svg.go | 60 +++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 2a500574b..8ad3e6b30 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -496,23 +496,19 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin } // Gets a certain line/curve's SVG path string. offsetIdx and pathData provides the points needed -func getSVGPathString(path *string, pathType *string, offsetIdx int, pathData []string) (string, error) { - var returnPath string - +func getSVGPathString(pathType *string, offsetIdx int, pathData []string) (string, error) { switch *pathType { case "M": - returnPath = fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) + return fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil case "L": - returnPath = fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]) + return fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil case "C": - returnPath = fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]) + return fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]), nil case "S": - returnPath = fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]) + return fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]), nil default: return "", fmt.Errorf("unknown svg path command \"%s\"", pathData[offsetIdx]) } - - return returnPath, nil } // Gets how much to increment by on an SVG string to get to the next path command @@ -568,17 +564,18 @@ func pathLength(pathData []string) (float64, error) { prevPosition = geo.Point{X: x, Y: y} incr, err := getPathStringIncrement(&pathData[i]) - increment = incr if err != nil { return 0, err } + + increment = incr } return pathLength, nil } -// Splits a SVG path into two SVG paths, with the first path being ~{percentage}% of the path +// Splits an SVG path into two SVG paths, with the first path being ~{percentage}% of the path func splitPath(path string, percentage float64) (string, string, error) { var sumPathLens, curPathLen, x, y float64 var prevPosition geo.Point @@ -618,21 +615,15 @@ func splitPath(path string, percentage float64) (string, string, error) { return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i]) } + curPath, err := getSVGPathString(&pathData[i], i, pathData) + if err != nil { + return "", "", err + } + if pastHalf { - curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData) path2 += curPath - - if err != nil { - return "", "", err - } - } else if sumPathLens+curPathLen < pathLen*percentage { - curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData) path1 += curPath - - if err != nil { - return "", "", err - } } sumPathLens += curPathLen @@ -671,12 +662,12 @@ func splitPath(path string, percentage float64) (string, string, error) { } incr, err := getPathStringIncrement(&pathData[i]) - increment = incr if err != nil { return "", "", err } + increment = incr prevPosition = geo.Point{X: x, Y: y} } @@ -758,17 +749,9 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co animatedClass = " animated-connection" } - // if connection is not animated or is a directed connection - if !connection.Animated || ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead)) { - pathEl := d2themes.NewThemableElement("path") - pathEl.D = path - pathEl.Fill = color.None - pathEl.Stroke = connection.Stroke - pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) - pathEl.Style = connection.CSSStyle() - pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) - fmt.Fprint(writer, pathEl.Render()) - } else { + // If connection is animated and bidirectional + if connection.Animated && ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead)) { + // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up path1, path2, err := splitPath(path, 0.5) if err != nil { @@ -793,6 +776,15 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co pathEl2.Style = connection.CSSStyle() pathEl2.Attributes = fmt.Sprintf("%s%s", markerEnd, mask) fmt.Fprint(writer, pathEl2.Render()) + } else { + pathEl := d2themes.NewThemableElement("path") + pathEl.D = path + pathEl.Fill = color.None + pathEl.Stroke = connection.Stroke + pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) + pathEl.Style = connection.CSSStyle() + pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask) + fmt.Fprint(writer, pathEl.Render()) } } From a9ee07708c9ed1d6aff79b4c902c4344a1400c6a Mon Sep 17 00:00:00 2001 From: Daniel Suh <23denial@gmail.com> Date: Sat, 18 May 2024 20:27:26 -0400 Subject: [PATCH 12/12] refactor --- d2renderers/d2svg/d2svg.go | 41 ++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 8ad3e6b30..9faf2a361 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -496,8 +496,8 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin } // Gets a certain line/curve's SVG path string. offsetIdx and pathData provides the points needed -func getSVGPathString(pathType *string, offsetIdx int, pathData []string) (string, error) { - switch *pathType { +func getSVGPathString(pathType string, offsetIdx int, pathData []string) (string, error) { + switch pathType { case "M": return fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil case "L": @@ -512,23 +512,19 @@ func getSVGPathString(pathType *string, offsetIdx int, pathData []string) (strin } // Gets how much to increment by on an SVG string to get to the next path command -func getPathStringIncrement(pathType *string) (int, error) { - var increment int - - switch *pathType { +func getPathStringIncrement(pathType string) (int, error) { + switch pathType { case "M": - increment = 3 + return 3, nil case "L": - increment = 3 + return 3, nil case "C": - increment = 7 + return 7, nil case "S": - increment = 5 + return 5, nil default: - return 0, fmt.Errorf("unknown svg path command \"%s\"", *pathType) + return 0, fmt.Errorf("unknown svg path command \"%s\"", pathType) } - - return increment, nil } // This function finds the length of a path in SVG notation @@ -563,7 +559,7 @@ func pathLength(pathData []string) (float64, error) { prevPosition = geo.Point{X: x, Y: y} - incr, err := getPathStringIncrement(&pathData[i]) + incr, err := getPathStringIncrement(pathData[i]) if err != nil { return 0, err @@ -591,7 +587,6 @@ func splitPath(path string, percentage float64) (string, string, error) { } for i := 0; i < len(pathData); i += increment { - switch pathData[i] { case "M": x, _ = strconv.ParseFloat(pathData[i+1], 64) @@ -615,20 +610,18 @@ func splitPath(path string, percentage float64) (string, string, error) { return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i]) } - curPath, err := getSVGPathString(&pathData[i], i, pathData) + curPath, err := getSVGPathString(pathData[i], i, pathData) if err != nil { return "", "", err } - if pastHalf { - path2 += curPath - } else if sumPathLens+curPathLen < pathLen*percentage { - path1 += curPath - } - sumPathLens += curPathLen - if !pastHalf && sumPathLens >= pathLen*percentage { + if pastHalf { // add to path2 + path2 += curPath + } else if sumPathLens < pathLen*percentage { // add to path1 + path1 += curPath + } else { // transition from path1 -> path2 t := (pathLen*percentage - sumPathLens + curPathLen) / curPathLen switch pathData[i] { @@ -661,7 +654,7 @@ func splitPath(path string, percentage float64) (string, string, error) { pastHalf = true } - incr, err := getPathStringIncrement(&pathData[i]) + incr, err := getPathStringIncrement(pathData[i]) if err != nil { return "", "", err