make paths normal

This commit is contained in:
Daniel Suh 2024-05-20 07:39:06 -04:00
parent 0a54cb48f1
commit 368fc938d1
No known key found for this signature in database
GPG key ID: 7548E646186EFE39
8 changed files with 548 additions and 455 deletions

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"unicode"
_ "embed" _ "embed"
@ -327,23 +328,71 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
if err != nil { if err != nil {
return "", err return "", err
} }
output := ""
finalPath := ""
insertSpacesAfterLetters := func(s string) {
output := ""
for i := 0; i < len(s); i++ {
if s[i] != ',' {
output += string(s[i])
}
if unicode.IsLetter(rune(s[i])) {
output += " "
}
}
fmt.Println("PATH", output)
finalPath += output
}
fmt.Println()
for i := 0; i < len(paths); i++ {
insertSpacesAfterLetters(paths[i])
}
animatedClass := "" animatedClass := ""
if connection.Animated { if connection.Animated {
animatedClass = " animated-connection" animatedClass = " animated-connection"
} }
pathEl := d2themes.NewThemableElement("path") // If connection is animated and bidirectional
pathEl.Fill = color.None if connection.Animated && ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead)) {
pathEl.Stroke = connection.Stroke // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass) path1, path2, err := svg.SplitPath(path, 0.5)
pathEl.Style = connection.CSSStyle()
pathEl.Attributes = attrs if err != nil {
for _, p := range paths { return "", err
pathEl.D = p }
output += 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.Style += "animation-direction: reverse;"
pathEl1.Attributes = attrs
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 = attrs
// return pathEl1.Render(), nil
return pathEl2.Render(), nil
// return pathEl1.Render() + " " + pathEl2.Render(), nil
} else {
pathEl := d2themes.NewThemableElement("path")
pathEl.D = finalPath
pathEl.Fill = color.None
pathEl.Stroke = connection.Stroke
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
pathEl.Style = connection.CSSStyle()
pathEl.Attributes = attrs
return pathEl.Render(), nil
} }
return output, nil
} }
// TODO cleanup // TODO cleanup

View file

@ -11,7 +11,6 @@ import (
"html" "html"
"io" "io"
"sort" "sort"
"strconv"
"strings" "strings"
"math" "math"
@ -495,178 +494,6 @@ 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 {
case "M":
return fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
case "L":
return fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
case "C":
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":
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])
}
}
// Gets how much to increment by on an SVG string to get to the next path command
func getPathStringIncrement(pathType string) (int, error) {
switch pathType {
case "M":
return 3, nil
case "L":
return 3, nil
case "C":
return 7, nil
case "S":
return 5, nil
default:
return 0, fmt.Errorf("unknown svg path command \"%s\"", pathType)
}
}
// This function finds the length of a path in SVG notation
func pathLength(pathData []string) (float64, error) {
var x, y, pathLength float64
var prevPosition geo.Point
var increment int
for i := 0; i < len(pathData); i += increment {
switch pathData[i] {
case "M":
x, _ = strconv.ParseFloat(pathData[i+1], 64)
y, _ = strconv.ParseFloat(pathData[i+2], 64)
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)
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)
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)
default:
return 0, fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
prevPosition = geo.Point{X: x, Y: y}
incr, err := getPathStringIncrement(pathData[i])
if err != nil {
return 0, err
}
increment = incr
}
return pathLength, nil
}
// 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
var path1, path2 string
var increment int
pastHalf := false
pathData := strings.Split(path, " ")
pathLen, err := pathLength(pathData)
if err != nil {
return "", "", err
}
for i := 0; i < len(pathData); i += increment {
switch pathData[i] {
case "M":
x, _ = strconv.ParseFloat(pathData[i+1], 64)
y, _ = strconv.ParseFloat(pathData[i+2], 64)
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)
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)
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)
default:
return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
curPath, err := getSVGPathString(pathData[i], i, pathData)
if err != nil {
return "", "", err
}
sumPathLens += curPathLen
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] {
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)
heading1 := geo.Point{X: h1x, Y: h1y}
heading2 := geo.Point{X: h2x, Y: h2y}
nextPoint := geo.Point{X: x, Y: y}
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 = 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)
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])
default:
return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
pastHalf = true
}
incr, err := getPathStringIncrement(pathData[i])
if err != nil {
return "", "", err
}
increment = incr
prevPosition = geo.Point{X: x, Y: y}
}
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) { 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 := "" opacityStyle := ""
if connection.Opacity != 1.0 { if connection.Opacity != 1.0 {
@ -745,7 +572,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
// If connection is animated and bidirectional // 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)) { 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 // 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) path1, path2, err := svg.SplitPath(path, 0.5)
if err != nil { if err != nil {
return "", err return "", err

View file

@ -215,6 +215,12 @@ ok: {
} }
-- bidirectional_connection_animation -- -- bidirectional_connection_animation --
vars: {
d2-config: {
sketch: true
}
}
a <-> b: {style.animated: true} a <-> b: {style.animated: true}
a <-> c: {style.animated: true} a <-> c: {style.animated: true}
a <-> d: {style.animated: true} a <-> d: {style.animated: true}

View file

@ -1,16 +1,24 @@
{ {
"name": "", "name": "",
"config": {
"sketch": true,
"themeID": null,
"darkThemeID": null,
"pad": null,
"center": null,
"layoutEngine": null
},
"isFolderOnly": false, "isFolderOnly": false,
"fontFamily": "SourceSansPro", "fontFamily": "HandDrawn",
"shapes": [ "shapes": [
{ {
"id": "a", "id": "a",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 170, "x": 172,
"y": 0 "y": 0
}, },
"width": 53, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -38,7 +46,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -51,7 +59,7 @@
"x": 0, "x": 0,
"y": 166 "y": 166
}, },
"width": 53, "width": 55,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -79,7 +87,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 10,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -89,10 +97,10 @@
"id": "c", "id": "c",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 113, "x": 115,
"y": 166 "y": 166
}, },
"width": 53, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -120,7 +128,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -130,10 +138,10 @@
"id": "d", "id": "d",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 226, "x": 229,
"y": 166 "y": 166
}, },
"width": 54, "width": 55,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -161,7 +169,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 9, "labelWidth": 10,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -171,7 +179,7 @@
"id": "e", "id": "e",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 340, "x": 344,
"y": 166 "y": 166
}, },
"width": 53, "width": 53,
@ -212,10 +220,10 @@
"id": "f", "id": "f",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 455, "x": 457,
"y": 0 "y": 0
}, },
"width": 51, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -243,7 +251,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 6, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -253,7 +261,7 @@
"id": "g", "id": "g",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 453, "x": 457,
"y": 166 "y": 166
}, },
"width": 54, "width": 54,
@ -294,10 +302,10 @@
"id": "x", "id": "x",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 566, "x": 571,
"y": 0 "y": 0
}, },
"width": 53, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -325,7 +333,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -358,19 +366,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 169.75, "x": 172.5,
"y": 45.957000732421875 "y": 46
}, },
{ {
"x": 55.14899826049805, "x": 56.5,
"y": 101.99099731445312 "y": 102
}, },
{ {
"x": 26.5, "x": 27.5,
"y": 126 "y": 126
}, },
{ {
"x": 26.5, "x": 27.5,
"y": 166 "y": 166
} }
], ],
@ -405,19 +413,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 173.5, "x": 176,
"y": 66 "y": 66
}, },
{ {
"x": 146.3000030517578, "x": 148.8000030517578,
"y": 106 "y": 106
}, },
{ {
"x": 139.5, "x": 142,
"y": 126 "y": 126
}, },
{ {
"x": 139.5, "x": 142,
"y": 166 "y": 166
} }
], ],
@ -452,19 +460,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 219, "x": 222.5,
"y": 66 "y": 66
}, },
{ {
"x": 246.1999969482422, "x": 249.6999969482422,
"y": 106 "y": 106
}, },
{ {
"x": 253, "x": 256.5,
"y": 126 "y": 126
}, },
{ {
"x": 253, "x": 256.5,
"y": 166 "y": 166
} }
], ],
@ -499,19 +507,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 222.5, "x": 226.25,
"y": 46 "y": 46.08599853515625
}, },
{ {
"x": 337.70001220703125, "x": 341.6499938964844,
"y": 102 "y": 102.01699829101562
}, },
{ {
"x": 366.5, "x": 370.5,
"y": 126 "y": 126
}, },
{ {
"x": 366.5, "x": 370.5,
"y": 166 "y": 166
} }
], ],
@ -546,19 +554,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 480, "x": 484,
"y": 66 "y": 66
}, },
{ {
"x": 480, "x": 484,
"y": 106 "y": 106
}, },
{ {
"x": 480, "x": 484,
"y": 126 "y": 126
}, },
{ {
"x": 480, "x": 484,
"y": 166 "y": 166
} }
], ],
@ -593,55 +601,55 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 619.166015625, "x": 624.666015625,
"y": 16 "y": 16
}, },
{ {
"x": 639.9660034179688, "x": 646.2659912109375,
"y": 3.1989998817443848 "y": 3.1989998817443848
}, },
{ {
"x": 646.5, "x": 653,
"y": 0 "y": 0
}, },
{ {
"x": 648.5, "x": 655,
"y": 0 "y": 0
}, },
{ {
"x": 650.5, "x": 657,
"y": 0 "y": 0
}, },
{ {
"x": 653.166015625, "x": 659.666015625,
"y": 6.599999904632568 "y": 6.599999904632568
}, },
{ {
"x": 655.166015625, "x": 661.666015625,
"y": 16.5 "y": 16.5
}, },
{ {
"x": 657.166015625, "x": 663.666015625,
"y": 26.399999618530273 "y": 26.399999618530273
}, },
{ {
"x": 657.166015625, "x": 663.666015625,
"y": 39.599998474121094 "y": 39.599998474121094
}, },
{ {
"x": 655.166015625, "x": 661.666015625,
"y": 49.5 "y": 49.5
}, },
{ {
"x": 653.166015625, "x": 659.666015625,
"y": 59.400001525878906 "y": 59.400001525878906
}, },
{ {
"x": 639.9660034179688, "x": 646.2659912109375,
"y": 62.79999923706055 "y": 62.79999923706055
}, },
{ {
"x": 619.166015625, "x": 624.666015625,
"y": 50 "y": 50
} }
], ],

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -1,13 +1,21 @@
{ {
"name": "", "name": "",
"config": {
"sketch": true,
"themeID": null,
"darkThemeID": null,
"pad": null,
"center": null,
"layoutEngine": null
},
"isFolderOnly": false, "isFolderOnly": false,
"fontFamily": "SourceSansPro", "fontFamily": "HandDrawn",
"shapes": [ "shapes": [
{ {
"id": "a", "id": "a",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 68, "x": 71,
"y": 12 "y": 12
}, },
"width": 160, "width": 160,
@ -38,7 +46,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -51,7 +59,7 @@
"x": 12, "x": 12,
"y": 208 "y": 208
}, },
"width": 53, "width": 55,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -79,7 +87,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 10,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -89,10 +97,10 @@
"id": "c", "id": "c",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 85, "x": 87,
"y": 208 "y": 208
}, },
"width": 53, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -120,7 +128,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -130,10 +138,10 @@
"id": "d", "id": "d",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 158, "x": 161,
"y": 208 "y": 208
}, },
"width": 54, "width": 55,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -161,7 +169,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 9, "labelWidth": 10,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -171,7 +179,7 @@
"id": "e", "id": "e",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 232, "x": 236,
"y": 208 "y": 208
}, },
"width": 53, "width": 53,
@ -212,10 +220,10 @@
"id": "f", "id": "f",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 306, "x": 309,
"y": 12 "y": 12
}, },
"width": 51, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -243,7 +251,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 6, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -253,7 +261,7 @@
"id": "g", "id": "g",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 305, "x": 309,
"y": 208 "y": 208
}, },
"width": 54, "width": 54,
@ -294,10 +302,10 @@
"id": "x", "id": "x",
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 427, "x": 433,
"y": 12 "y": 12
}, },
"width": 53, "width": 54,
"height": 66, "height": 66,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
@ -325,7 +333,7 @@
"italic": false, "italic": false,
"bold": true, "bold": true,
"underline": false, "underline": false,
"labelWidth": 8, "labelWidth": 9,
"labelHeight": 21, "labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER", "labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0, "zIndex": 0,
@ -358,19 +366,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 100.25, "x": 103.25,
"y": 78 "y": 78
}, },
{ {
"x": 100.25, "x": 103.25,
"y": 118 "y": 118
}, },
{ {
"x": 38.5, "x": 39.5,
"y": 118 "y": 118
}, },
{ {
"x": 38.5, "x": 39.5,
"y": 208 "y": 208
} }
], ],
@ -404,19 +412,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 132.25, "x": 135.25,
"y": 78 "y": 78
}, },
{ {
"x": 132.25, "x": 135.25,
"y": 168 "y": 168
}, },
{ {
"x": 111.5, "x": 114,
"y": 168 "y": 168
}, },
{ {
"x": 111.5, "x": 114,
"y": 208 "y": 208
} }
], ],
@ -450,19 +458,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 164.25, "x": 167.25,
"y": 78 "y": 78
}, },
{ {
"x": 164.25, "x": 167.25,
"y": 168 "y": 168
}, },
{ {
"x": 185, "x": 188.5,
"y": 168 "y": 168
}, },
{ {
"x": 185, "x": 188.5,
"y": 208 "y": 208
} }
], ],
@ -496,19 +504,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 196.25, "x": 199.25,
"y": 78 "y": 78
}, },
{ {
"x": 196.25, "x": 199.25,
"y": 118 "y": 118
}, },
{ {
"x": 258.5, "x": 262.5,
"y": 118 "y": 118
}, },
{ {
"x": 258.5, "x": 262.5,
"y": 208 "y": 208
} }
], ],
@ -542,11 +550,11 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 332, "x": 336,
"y": 78 "y": 78
}, },
{ {
"x": 332, "x": 336,
"y": 208 "y": 208
} }
], ],
@ -580,19 +588,19 @@
"labelPercentage": 0, "labelPercentage": 0,
"route": [ "route": [
{ {
"x": 427.5, "x": 433,
"y": 34 "y": 34
}, },
{ {
"x": 377.5, "x": 383,
"y": 34 "y": 34
}, },
{ {
"x": 377.5, "x": 383,
"y": 56 "y": 56
}, },
{ {
"x": 427.5, "x": 433,
"y": 56 "y": 56
} }
], ],

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -3,6 +3,7 @@ package svg
import ( import (
"fmt" "fmt"
"math" "math"
"strconv"
"strings" "strings"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
@ -139,3 +140,181 @@ func BezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, g
return q1, q2, q3, q4 return q1, q2, q3, q4
} }
// 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 {
case "M":
return fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
case "L":
return fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
case "C":
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":
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])
}
}
// Gets how much to increment by on an SVG string to get to the next path command
func getPathStringIncrement(pathType string) (int, error) {
switch pathType {
case "M":
return 3, nil
case "L":
return 3, nil
case "C":
return 7, nil
case "S":
return 5, nil
default:
return 0, fmt.Errorf("unknown svg path command \"%s\"", pathType)
}
}
// This function finds the length of a path in SVG notation
func pathLength(pathData []string) (float64, error) {
var x, y, pathLength float64
var prevPosition geo.Point
var increment int
for i := 0; i < len(pathData); i += increment {
switch pathData[i] {
case "M":
x, _ = strconv.ParseFloat(pathData[i+1], 64)
y, _ = strconv.ParseFloat(pathData[i+2], 64)
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)
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)
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)
default:
fmt.Println("hello", pathData[i])
return 0, fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
prevPosition = geo.Point{X: x, Y: y}
incr, err := getPathStringIncrement(pathData[i])
if err != nil {
return 0, err
}
increment = incr
}
return pathLength, nil
}
// 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
var path1, path2 string
var increment int
pastHalf := false
pathData := strings.Split(path, " ")
pathLen, err := pathLength(pathData)
fmt.Println("pathLen:", pathLen)
if err != nil {
return "", "", err
}
for i := 0; i < len(pathData); i += increment {
switch pathData[i] {
case "M":
x, _ = strconv.ParseFloat(pathData[i+1], 64)
y, _ = strconv.ParseFloat(pathData[i+2], 64)
curPathLen = 0
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)
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)
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)
default:
return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
curPath, err := getSVGPathString(pathData[i], i, pathData)
if err != nil {
return "", "", err
}
sumPathLens += curPathLen
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] {
case "M":
path2 += fmt.Sprintf("M %s %s ", pathData[i+3], pathData[i+4])
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)
heading1 := geo.Point{X: h1x, Y: h1y}
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)
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)
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":
// 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:
return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
pastHalf = true
}
incr, err := getPathStringIncrement(pathData[i])
if err != nil {
return "", "", err
}
increment = incr
prevPosition = geo.Point{X: x, Y: y}
}
return path1, path2, nil
}