diff --git a/d2graph/cyclediagram.go b/d2graph/cyclediagram.go
new file mode 100644
index 0000000000..a8f8e0b1ad
--- /dev/null
+++ b/d2graph/cyclediagram.go
@@ -0,0 +1,7 @@
+package d2graph
+
+import "oss.terrastruct.com/d2/d2target"
+
+func (obj *Object) IsCycleDiagram() bool {
+ return obj != nil && obj.Shape.Value == d2target.ShapeCycleDiagram
+}
diff --git a/d2graph/grid_diagram.go b/d2graph/griddiagram.go
similarity index 100%
rename from d2graph/grid_diagram.go
rename to d2graph/griddiagram.go
diff --git a/d2graph/seqdiagram.go b/d2graph/sequencediagram.go
similarity index 100%
rename from d2graph/seqdiagram.go
rename to d2graph/sequencediagram.go
diff --git a/d2layouts/d2cycle/layout.go b/d2layouts/d2cycle/layout.go
new file mode 100644
index 0000000000..d957c68142
--- /dev/null
+++ b/d2layouts/d2cycle/layout.go
@@ -0,0 +1,241 @@
+package d2cycle
+
+import (
+ "context"
+ "math"
+
+ "oss.terrastruct.com/d2/d2graph"
+ "oss.terrastruct.com/d2/lib/geo"
+ "oss.terrastruct.com/d2/lib/label"
+ "oss.terrastruct.com/util-go/go2"
+)
+
+const (
+ MIN_RADIUS = 200
+ PADDING = 20
+ MIN_SEGMENT_LEN = 10
+ ARC_STEPS = 30 // high resolution for smooth arcs
+
+)
+
+// Layout arranges nodes in a circle, ensures label/icon positions are set,
+// then routes edges with arcs that get clipped at node borders.
+func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
+ objects := g.Root.ChildrenArray
+ if len(objects) == 0 {
+ return nil
+ }
+
+ // Make sure every object that has label/icon also has a default position
+ for _, obj := range g.Objects {
+ positionLabelsIcons(obj)
+ }
+
+ // Arrange objects in a circle
+ radius := calculateRadius(objects)
+ positionObjects(objects, radius)
+
+ // Create arcs
+ for _, edge := range g.Edges {
+ createCircularArc(edge)
+ }
+
+ return nil
+}
+
+func calculateRadius(objects []*d2graph.Object) float64 {
+ numObjects := float64(len(objects))
+ maxSize := 0.0
+ for _, obj := range objects {
+ size := math.Max(obj.Box.Width, obj.Box.Height)
+ maxSize = math.Max(maxSize, size)
+ }
+ // ensure enough radius to fit all objects
+ minRadius := (maxSize/2.0 + PADDING) / math.Sin(math.Pi/numObjects)
+ return math.Max(minRadius, MIN_RADIUS)
+}
+
+func positionObjects(objects []*d2graph.Object, radius float64) {
+ numObjects := float64(len(objects))
+ // Offset so i=0 is top-center
+ angleOffset := -math.Pi / 2
+
+ for i, obj := range objects {
+ angle := angleOffset + (2 * math.Pi * float64(i) / numObjects)
+
+ x := radius * math.Cos(angle)
+ y := radius * math.Sin(angle)
+
+ // center the box at (x, y)
+ obj.TopLeft = geo.NewPoint(
+ x-obj.Box.Width/2,
+ y-obj.Box.Height/2,
+ )
+ }
+}
+
+// createCircularArc samples a smooth arc from center to center, then
+// forces the endpoints onto each shape's border, and finally calls
+// TraceToShape to clip any additional overrun.
+func createCircularArc(edge *d2graph.Edge) {
+ if edge.Src == nil || edge.Dst == nil {
+ return
+ }
+
+ srcCenter := edge.Src.Center()
+ dstCenter := edge.Dst.Center()
+
+ // angles from origin
+ srcAngle := math.Atan2(srcCenter.Y, srcCenter.X)
+ dstAngle := math.Atan2(dstCenter.Y, dstCenter.X)
+ if dstAngle < srcAngle {
+ dstAngle += 2 * math.Pi
+ }
+
+ arcRadius := math.Hypot(srcCenter.X, srcCenter.Y)
+
+ // Sample points along the arc
+ path := make([]*geo.Point, 0, ARC_STEPS+1)
+ for i := 0; i <= ARC_STEPS; i++ {
+ t := float64(i) / float64(ARC_STEPS)
+ angle := srcAngle + t*(dstAngle-srcAngle)
+ x := arcRadius * math.Cos(angle)
+ y := arcRadius * math.Sin(angle)
+ path = append(path, geo.NewPoint(x, y))
+ }
+ // Set start/end to exact centers
+ path[0] = srcCenter
+ path[len(path)-1] = dstCenter
+
+ // Use TraceToShape to clip route to node borders
+ edge.Route = path
+ startIndex, endIndex := edge.TraceToShape(edge.Route, 0, len(edge.Route)-1)
+ if startIndex < endIndex {
+ edge.Route = edge.Route[startIndex : endIndex+1]
+ }
+ edge.IsCurve = true
+}
+
+// clampPointOutsideBox walks forward from 'startIdx' until the path segment
+// leaves the bounding box. Then it sets path[startIdx] to the intersection.
+// If we never find it, we return (startIdx, path[startIdx]) meaning we can't clamp.
+func clampPointOutsideBox(box *geo.Box, path []*geo.Point, startIdx int) (int, *geo.Point) {
+ if startIdx >= len(path)-1 {
+ return startIdx, path[startIdx]
+ }
+ // If path[startIdx] is outside, no clamp needed
+ if !boxContains(box, path[startIdx]) {
+ return startIdx, path[startIdx]
+ }
+
+ // Walk forward looking for outside
+ for i := startIdx + 1; i < len(path); i++ {
+ insideNext := boxContains(box, path[i])
+ if insideNext {
+ // still inside -> keep going
+ continue
+ }
+ // crossing from inside to outside between path[i-1], path[i]
+ seg := geo.NewSegment(path[i-1], path[i])
+ inters := boxIntersections(box, *seg)
+ if len(inters) > 0 {
+ // use first intersection
+ return i, inters[0]
+ }
+ // fallback => no intersection found
+ return i, path[i]
+ }
+ // entire remainder is inside, so we can't clamp
+ // Just return the end
+ last := len(path) - 1
+ return last, path[last]
+}
+
+// clampPointOutsideBoxReverse scans backward from endIdx while path[j] is in the box.
+// Once we find crossing (outside→inside), we return (j, intersection).
+func clampPointOutsideBoxReverse(box *geo.Box, path []*geo.Point, endIdx int) (int, *geo.Point) {
+ if endIdx <= 0 {
+ return endIdx, path[endIdx]
+ }
+ if !boxContains(box, path[endIdx]) {
+ // already outside
+ return endIdx, path[endIdx]
+ }
+
+ for j := endIdx - 1; j >= 0; j-- {
+ if boxContains(box, path[j]) {
+ continue
+ }
+ // crossing from outside -> inside between path[j], path[j+1]
+ seg := geo.NewSegment(path[j], path[j+1])
+ inters := boxIntersections(box, *seg)
+ if len(inters) > 0 {
+ return j, inters[0]
+ }
+ return j, path[j]
+ }
+
+ // entire path inside
+ return 0, path[0]
+}
+
+// Helper if your geo.Box doesn’t implement Contains()
+func boxContains(b *geo.Box, p *geo.Point) bool {
+ // typical bounding-box check
+ return p.X >= b.TopLeft.X &&
+ p.X <= b.TopLeft.X+b.Width &&
+ p.Y >= b.TopLeft.Y &&
+ p.Y <= b.TopLeft.Y+b.Height
+}
+
+// Helper if your geo.Box doesn’t implement Intersections(geo.Segment) yet
+func boxIntersections(b *geo.Box, seg geo.Segment) []*geo.Point {
+ // We'll assume d2's standard geo.Box has a built-in Intersections(*Segment) method.
+ // If not, implement manually. For example, checking each of the 4 edges:
+ // left, right, top, bottom
+ // For simplicity, if you do have b.Intersections(...) you can just do:
+ // return b.Intersections(seg)
+ return b.Intersections(seg)
+ // If you don't have that, you'd code the line-rect intersection yourself.
+}
+
+// positionLabelsIcons is basically your logic that sets default label/icon positions if needed
+func positionLabelsIcons(obj *d2graph.Object) {
+ // If there's an icon but no icon position, give it a default
+ if obj.Icon != nil && obj.IconPosition == nil {
+ if len(obj.ChildrenArray) > 0 {
+ obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
+ if obj.LabelPosition == nil {
+ obj.LabelPosition = go2.Pointer(label.OutsideTopRight.String())
+ return
+ }
+ } else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
+ obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
+ } else {
+ obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
+ }
+ }
+
+ // If there's a label but no label position, give it a default
+ if obj.HasLabel() && obj.LabelPosition == nil {
+ if len(obj.ChildrenArray) > 0 {
+ obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
+ } else if obj.HasOutsideBottomLabel() {
+ obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
+ } else if obj.Icon != nil {
+ obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
+ } else {
+ obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
+ }
+
+ // If the label is bigger than the shape, fallback to outside positions
+ if float64(obj.LabelDimensions.Width) > obj.Width ||
+ float64(obj.LabelDimensions.Height) > obj.Height {
+ if len(obj.ChildrenArray) > 0 {
+ obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
+ } else {
+ obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
+ }
+ }
+ }
+}
diff --git a/d2layouts/d2layouts.go b/d2layouts/d2layouts.go
index 87874a6b41..b4eff3b9fc 100644
--- a/d2layouts/d2layouts.go
+++ b/d2layouts/d2layouts.go
@@ -9,6 +9,7 @@ import (
"strings"
"oss.terrastruct.com/d2/d2graph"
+ "oss.terrastruct.com/d2/d2layouts/d2cycle"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
@@ -20,12 +21,12 @@ import (
type DiagramType string
-// a grid diagram at a constant near is
const (
DefaultGraphType DiagramType = ""
ConstantNearGraph DiagramType = "constant-near"
GridDiagram DiagramType = "grid-diagram"
SequenceDiagram DiagramType = "sequence-diagram"
+ CycleDiagram DiagramType = "cycle-diagram"
)
type GraphInfo struct {
@@ -260,6 +261,12 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
if err != nil {
return err
}
+ case CycleDiagram:
+ log.Debug(ctx, "layout sequence", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
+ err = d2cycle.Layout(ctx, g, coreLayout)
+ if err != nil {
+ return err
+ }
default:
log.Debug(ctx, "default layout", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
err := coreLayout(ctx, g)
@@ -360,6 +367,8 @@ func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
gi.DiagramType = SequenceDiagram
} else if obj.IsGridDiagram() {
gi.DiagramType = GridDiagram
+ } else if obj.IsCycleDiagram() {
+ gi.DiagramType = CycleDiagram
}
return gi
}
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 153b7b3451..a42ee3681b 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -452,37 +452,78 @@ func getArrowheadAdjustments(connection d2target.Connection, idToShape map[strin
func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string {
var path []string
route := connection.Route
+ if len(route) == 0 {
+ return ""
+ }
+ // Move command to start
path = append(path, fmt.Sprintf("M %f %f",
route[0].X+srcAdj.X,
route[0].Y+srcAdj.Y,
))
if connection.IsCurve {
+ // If we don't have enough points to do triple-step, handle small fallback
+ if len(route) < 3 {
+ // If only 1 or 2 points in route, just draw lines
+ for _, p := range route[1:] {
+ path = append(path, fmt.Sprintf("L %f %f",
+ p.X+dstAdj.X, p.Y+dstAdj.Y,
+ ))
+ }
+ return strings.Join(path, " ")
+ }
+
i := 1
- for ; i < len(route)-3; i += 3 {
+ // Process triple curves in steps of 3
+ for ; i+2 < len(route)-1; i += 3 {
path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
route[i].X, route[i].Y,
route[i+1].X, route[i+1].Y,
route[i+2].X, route[i+2].Y,
))
}
- // final curve target adjustment
- path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
- route[i].X, route[i].Y,
- route[i+1].X, route[i+1].Y,
- route[i+2].X+dstAdj.X,
- route[i+2].Y+dstAdj.Y,
- ))
+
+ // Now handle the “final” curve to last point
+ // Make sure i+2 is still within range
+ if i+2 < len(route) {
+ // last triple
+ path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
+ route[i].X, route[i].Y,
+ route[i+1].X, route[i+1].Y,
+ route[i+2].X+dstAdj.X, // final point plus dst adjustment
+ route[i+2].Y+dstAdj.Y,
+ ))
+ } else if i+1 < len(route) {
+ // We have i+1 but not i+2 => do a simpler final curve or line
+ path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
+ route[i].X, route[i].Y,
+ route[i].X, route[i].Y, // repeated for control
+ route[i+1].X+dstAdj.X,
+ route[i+1].Y+dstAdj.Y,
+ ))
+ } else {
+ // We have no final triple => do nothing or fallback line
+ }
} else {
+ // Not a curve => the "rounded corner" logic
for i := 1; i < len(route)-1; i++ {
prevSource := route[i-1]
prevTarget := route[i]
currTarget := route[i+1]
+
+ // Make sure i+1 is valid
+ if i+1 >= len(route) {
+ break
+ }
+
prevVector := prevSource.VectorTo(prevTarget)
currVector := prevTarget.VectorTo(currTarget)
- dist := geo.EuclideanDistance(prevTarget.X, prevTarget.Y, currTarget.X, currTarget.Y)
+ dist := geo.EuclideanDistance(
+ prevTarget.X, prevTarget.Y,
+ currTarget.X, currTarget.Y,
+ )
connectionBorderRadius := connection.BorderRadius
units := math.Min(connectionBorderRadius, dist/2)
@@ -490,20 +531,26 @@ func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string
prevTranslations := prevVector.Unit().Multiply(units).ToPoint()
currTranslations := currVector.Unit().Multiply(units).ToPoint()
+ // Move to corner with "L"
path = append(path, fmt.Sprintf("L %f %f",
prevTarget.X-prevTranslations.X,
prevTarget.Y-prevTranslations.Y,
))
- // If the segment length is too small, instead of drawing 2 arcs, just skip this segment and bezier curve to the next one
if units < connectionBorderRadius && i < len(route)-2 {
+ // Next checks i+2 => ensure it’s in range
+ if i+2 >= len(route) {
+ // can't do nextTarget => break or do fallback
+ continue
+ }
nextTarget := route[i+2]
- nextVector := geo.NewVector(nextTarget.X-currTarget.X, nextTarget.Y-currTarget.Y)
- i++
+ nextVector := geo.NewVector(
+ nextTarget.X-currTarget.X,
+ nextTarget.Y-currTarget.Y,
+ )
+ i++ // skip next point
nextTranslations := nextVector.Unit().Multiply(units).ToPoint()
- // These 2 bezier control points aren't just at the corner -- they are reflected at the corner, which causes the curve to be ~tangent to the corner,
- // which matches how the two arcs look
path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
// Control point
prevTarget.X+prevTranslations.X,
@@ -511,7 +558,7 @@ func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string
// Control point
currTarget.X-nextTranslations.X,
currTarget.Y-nextTranslations.Y,
- // Where curve ends
+ // End
currTarget.X+nextTranslations.X,
currTarget.Y+nextTranslations.Y,
))
@@ -525,11 +572,14 @@ func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string
}
}
- lastPoint := route[len(route)-1]
- path = append(path, fmt.Sprintf("L %f %f",
- lastPoint.X+dstAdj.X,
- lastPoint.Y+dstAdj.Y,
- ))
+ // Finally, draw a line to the last route point + dst offset
+ if len(route) > 1 {
+ lastPoint := route[len(route)-1]
+ path = append(path, fmt.Sprintf("L %f %f",
+ lastPoint.X+dstAdj.X,
+ lastPoint.Y+dstAdj.Y,
+ ))
+ }
}
return strings.Join(path, " ")
diff --git a/d2target/d2target.go b/d2target/d2target.go
index 266e0f184e..68e7eb3313 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -942,6 +942,7 @@ const (
ShapeSQLTable = "sql_table"
ShapeImage = "image"
ShapeSequenceDiagram = "sequence_diagram"
+ ShapeCycleDiagram = "cycle"
ShapeHierarchy = "hierarchy"
)
@@ -969,6 +970,7 @@ var Shapes = []string{
ShapeSQLTable,
ShapeImage,
ShapeSequenceDiagram,
+ ShapeCycleDiagram,
ShapeHierarchy,
}
@@ -1037,6 +1039,7 @@ var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{
ShapeSQLTable: shape.TABLE_TYPE,
ShapeImage: shape.IMAGE_TYPE,
ShapeSequenceDiagram: shape.SQUARE_TYPE,
+ ShapeCycleDiagram: shape.SQUARE_TYPE,
ShapeHierarchy: shape.SQUARE_TYPE,
}
diff --git a/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
new file mode 100644
index 0000000000..2abd9446ac
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
@@ -0,0 +1,1495 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "1",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 453,
+ "height": 466,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "1.a",
+ "type": "rectangle",
+ "pos": {
+ "x": -26,
+ "y": -233
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 173,
+ "y": -33
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.c",
+ "type": "rectangle",
+ "pos": {
+ "x": -26,
+ "y": 167
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.d",
+ "type": "rectangle",
+ "pos": {
+ "x": -227,
+ "y": -32
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2",
+ "type": "cycle",
+ "pos": {
+ "x": 513,
+ "y": 50
+ },
+ "width": 399,
+ "height": 366,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "2.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 486,
+ "y": -183
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 659,
+ "y": 116
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 313,
+ "y": 117
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "3",
+ "type": "cycle",
+ "pos": {
+ "x": 972,
+ "y": 0
+ },
+ "width": 53,
+ "height": 466,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "3.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 945,
+ "y": -233
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "3.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 945,
+ "y": 167
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "1.(a -> b)[0]",
+ "src": "1.a",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 0,
+ "y": -200
+ },
+ {
+ "x": 10.467000007629395,
+ "y": -199.72500610351562
+ },
+ {
+ "x": 20.905000686645508,
+ "y": -198.9040069580078
+ },
+ {
+ "x": 31.285999298095703,
+ "y": -197.53700256347656
+ },
+ {
+ "x": 41.582000732421875,
+ "y": -195.62899780273438
+ },
+ {
+ "x": 51.76300048828125,
+ "y": -193.18499755859375
+ },
+ {
+ "x": 61.803001403808594,
+ "y": -190.21099853515625
+ },
+ {
+ "x": 71.6729965209961,
+ "y": -186.71600341796875
+ },
+ {
+ "x": 81.34700012207031,
+ "y": -182.70899963378906
+ },
+ {
+ "x": 90.7979965209961,
+ "y": -178.2010040283203
+ },
+ {
+ "x": 100,
+ "y": -173.2050018310547
+ },
+ {
+ "x": 108.927001953125,
+ "y": -167.73399353027344
+ },
+ {
+ "x": 117.55699920654297,
+ "y": -161.80299377441406
+ },
+ {
+ "x": 125.86399841308594,
+ "y": -155.4290008544922
+ },
+ {
+ "x": 133.8260040283203,
+ "y": -148.6280059814453
+ },
+ {
+ "x": 141.42100524902344,
+ "y": -141.42100524902344
+ },
+ {
+ "x": 148.6280059814453,
+ "y": -133.8260040283203
+ },
+ {
+ "x": 155.4290008544922,
+ "y": -125.86399841308594
+ },
+ {
+ "x": 161.80299377441406,
+ "y": -117.55699920654297
+ },
+ {
+ "x": 167.73399353027344,
+ "y": -108.927001953125
+ },
+ {
+ "x": 173.2050018310547,
+ "y": -100
+ },
+ {
+ "x": 178.2010040283203,
+ "y": -90.7979965209961
+ },
+ {
+ "x": 182.70899963378906,
+ "y": -81.34700012207031
+ },
+ {
+ "x": 186.71600341796875,
+ "y": -71.6729965209961
+ },
+ {
+ "x": 190.21099853515625,
+ "y": -61.803001403808594
+ },
+ {
+ "x": 193.18499755859375,
+ "y": -51.76300048828125
+ },
+ {
+ "x": 195.62899780273438,
+ "y": -41.582000732421875
+ },
+ {
+ "x": 197.53700256347656,
+ "y": -31.285999298095703
+ },
+ {
+ "x": 198.9040069580078,
+ "y": -20.905000686645508
+ },
+ {
+ "x": 199.72500610351562,
+ "y": -10.467000007629395
+ },
+ {
+ "x": 200,
+ "y": 0
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "1.(b -> c)[0]",
+ "src": "1.b",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 200,
+ "y": 0
+ },
+ {
+ "x": 199.72500610351562,
+ "y": 10.467000007629395
+ },
+ {
+ "x": 198.9040069580078,
+ "y": 20.905000686645508
+ },
+ {
+ "x": 197.53700256347656,
+ "y": 31.285999298095703
+ },
+ {
+ "x": 195.62899780273438,
+ "y": 41.582000732421875
+ },
+ {
+ "x": 193.18499755859375,
+ "y": 51.76300048828125
+ },
+ {
+ "x": 190.21099853515625,
+ "y": 61.803001403808594
+ },
+ {
+ "x": 186.71600341796875,
+ "y": 71.6729965209961
+ },
+ {
+ "x": 182.70899963378906,
+ "y": 81.34700012207031
+ },
+ {
+ "x": 178.2010040283203,
+ "y": 90.7979965209961
+ },
+ {
+ "x": 173.2050018310547,
+ "y": 99.9990005493164
+ },
+ {
+ "x": 167.73399353027344,
+ "y": 108.927001953125
+ },
+ {
+ "x": 161.80299377441406,
+ "y": 117.55699920654297
+ },
+ {
+ "x": 155.4290008544922,
+ "y": 125.86399841308594
+ },
+ {
+ "x": 148.6280059814453,
+ "y": 133.8260040283203
+ },
+ {
+ "x": 141.42100524902344,
+ "y": 141.42100524902344
+ },
+ {
+ "x": 133.8260040283203,
+ "y": 148.6280059814453
+ },
+ {
+ "x": 125.86399841308594,
+ "y": 155.4290008544922
+ },
+ {
+ "x": 117.55699920654297,
+ "y": 161.80299377441406
+ },
+ {
+ "x": 108.927001953125,
+ "y": 167.73399353027344
+ },
+ {
+ "x": 100,
+ "y": 173.2050018310547
+ },
+ {
+ "x": 90.7979965209961,
+ "y": 178.2010040283203
+ },
+ {
+ "x": 81.34700012207031,
+ "y": 182.70899963378906
+ },
+ {
+ "x": 71.6729965209961,
+ "y": 186.71600341796875
+ },
+ {
+ "x": 61.803001403808594,
+ "y": 190.21099853515625
+ },
+ {
+ "x": 51.76300048828125,
+ "y": 193.18499755859375
+ },
+ {
+ "x": 41.582000732421875,
+ "y": 195.62899780273438
+ },
+ {
+ "x": 31.285999298095703,
+ "y": 197.53700256347656
+ },
+ {
+ "x": 20.905000686645508,
+ "y": 198.9040069580078
+ },
+ {
+ "x": 10.467000007629395,
+ "y": 199.72500610351562
+ },
+ {
+ "x": 0,
+ "y": 200
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "1.(c -> d)[0]",
+ "src": "1.c",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 0,
+ "y": 200
+ },
+ {
+ "x": -10.467000007629395,
+ "y": 199.72500610351562
+ },
+ {
+ "x": -20.905000686645508,
+ "y": 198.9040069580078
+ },
+ {
+ "x": -31.285999298095703,
+ "y": 197.53700256347656
+ },
+ {
+ "x": -41.582000732421875,
+ "y": 195.62899780273438
+ },
+ {
+ "x": -51.76300048828125,
+ "y": 193.18499755859375
+ },
+ {
+ "x": -61.803001403808594,
+ "y": 190.21099853515625
+ },
+ {
+ "x": -71.6729965209961,
+ "y": 186.71600341796875
+ },
+ {
+ "x": -81.34700012207031,
+ "y": 182.70899963378906
+ },
+ {
+ "x": -90.7979965209961,
+ "y": 178.2010040283203
+ },
+ {
+ "x": -99.9990005493164,
+ "y": 173.2050018310547
+ },
+ {
+ "x": -108.927001953125,
+ "y": 167.73399353027344
+ },
+ {
+ "x": -117.55699920654297,
+ "y": 161.80299377441406
+ },
+ {
+ "x": -125.86399841308594,
+ "y": 155.4290008544922
+ },
+ {
+ "x": -133.8260040283203,
+ "y": 148.6280059814453
+ },
+ {
+ "x": -141.42100524902344,
+ "y": 141.42100524902344
+ },
+ {
+ "x": -148.6280059814453,
+ "y": 133.8260040283203
+ },
+ {
+ "x": -155.4290008544922,
+ "y": 125.86399841308594
+ },
+ {
+ "x": -161.80299377441406,
+ "y": 117.55699920654297
+ },
+ {
+ "x": -167.73399353027344,
+ "y": 108.927001953125
+ },
+ {
+ "x": -173.2050018310547,
+ "y": 99.9990005493164
+ },
+ {
+ "x": -178.2010040283203,
+ "y": 90.7979965209961
+ },
+ {
+ "x": -182.70899963378906,
+ "y": 81.34700012207031
+ },
+ {
+ "x": -186.71600341796875,
+ "y": 71.6729965209961
+ },
+ {
+ "x": -190.21099853515625,
+ "y": 61.803001403808594
+ },
+ {
+ "x": -193.18499755859375,
+ "y": 51.76300048828125
+ },
+ {
+ "x": -195.62899780273438,
+ "y": 41.582000732421875
+ },
+ {
+ "x": -197.53700256347656,
+ "y": 31.285999298095703
+ },
+ {
+ "x": -198.9040069580078,
+ "y": 20.905000686645508
+ },
+ {
+ "x": -199.72500610351562,
+ "y": 10.467000007629395
+ },
+ {
+ "x": -200,
+ "y": 0
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "2.(a -> b)[0]",
+ "src": "2.a",
+ "srcArrow": "none",
+ "dst": "2.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,
+ "link": "",
+ "route": [
+ {
+ "x": 513,
+ "y": -150
+ },
+ {
+ "x": 526.9509887695312,
+ "y": -149.51199340820312
+ },
+ {
+ "x": 540.833984375,
+ "y": -148.05299377441406
+ },
+ {
+ "x": 554.5819702148438,
+ "y": -145.62899780273438
+ },
+ {
+ "x": 568.1270141601562,
+ "y": -142.2519989013672
+ },
+ {
+ "x": 581.4039916992188,
+ "y": -137.93800354003906
+ },
+ {
+ "x": 594.3469848632812,
+ "y": -132.70899963378906
+ },
+ {
+ "x": 606.8939819335938,
+ "y": -126.58899688720703
+ },
+ {
+ "x": 618.9829711914062,
+ "y": -119.60900115966797
+ },
+ {
+ "x": 630.5570068359375,
+ "y": -111.8030014038086
+ },
+ {
+ "x": 641.5570068359375,
+ "y": -103.20800018310547
+ },
+ {
+ "x": 651.9310302734375,
+ "y": -93.86699676513672
+ },
+ {
+ "x": 661.6279907226562,
+ "y": -83.82599639892578
+ },
+ {
+ "x": 670.6019897460938,
+ "y": -73.13200378417969
+ },
+ {
+ "x": 678.8070068359375,
+ "y": -61.8380012512207
+ },
+ {
+ "x": 686.2050170898438,
+ "y": -50
+ },
+ {
+ "x": 692.7579956054688,
+ "y": -37.67399978637695
+ },
+ {
+ "x": 698.4359741210938,
+ "y": -24.92099952697754
+ },
+ {
+ "x": 703.2109985351562,
+ "y": -11.803000450134277
+ },
+ {
+ "x": 707.0590209960938,
+ "y": 1.6150000095367432
+ },
+ {
+ "x": 709.9609985351562,
+ "y": 15.270000457763672
+ },
+ {
+ "x": 711.9039916992188,
+ "y": 29.0939998626709
+ },
+ {
+ "x": 712.8779907226562,
+ "y": 43.02000045776367
+ },
+ {
+ "x": 712.8779907226562,
+ "y": 56.979000091552734
+ },
+ {
+ "x": 711.9039916992188,
+ "y": 70.90499877929688
+ },
+ {
+ "x": 709.9609985351562,
+ "y": 84.72899627685547
+ },
+ {
+ "x": 707.0590209960938,
+ "y": 98.38400268554688
+ },
+ {
+ "x": 703.2109985351562,
+ "y": 111.8030014038086
+ },
+ {
+ "x": 698.4359741210938,
+ "y": 124.9209976196289
+ },
+ {
+ "x": 692.7579956054688,
+ "y": 137.6739959716797
+ },
+ {
+ "x": 686.2050170898438,
+ "y": 149.99899291992188
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "2.(b -> c)[0]",
+ "src": "2.b",
+ "srcArrow": "none",
+ "dst": "2.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,
+ "link": "",
+ "route": [
+ {
+ "x": 686.2050170898438,
+ "y": 149.99899291992188
+ },
+ {
+ "x": 678.8070068359375,
+ "y": 161.83799743652344
+ },
+ {
+ "x": 670.6019897460938,
+ "y": 173.1320037841797
+ },
+ {
+ "x": 661.6279907226562,
+ "y": 183.8260040283203
+ },
+ {
+ "x": 651.9310302734375,
+ "y": 193.86700439453125
+ },
+ {
+ "x": 641.5570068359375,
+ "y": 203.20799255371094
+ },
+ {
+ "x": 630.5570068359375,
+ "y": 211.80299377441406
+ },
+ {
+ "x": 618.9829711914062,
+ "y": 219.60899353027344
+ },
+ {
+ "x": 606.8939819335938,
+ "y": 226.58900451660156
+ },
+ {
+ "x": 594.3469848632812,
+ "y": 232.70899963378906
+ },
+ {
+ "x": 581.4039916992188,
+ "y": 237.93800354003906
+ },
+ {
+ "x": 568.1270141601562,
+ "y": 242.2519989013672
+ },
+ {
+ "x": 554.5819702148438,
+ "y": 245.62899780273438
+ },
+ {
+ "x": 540.833984375,
+ "y": 248.05299377441406
+ },
+ {
+ "x": 526.9509887695312,
+ "y": 249.51199340820312
+ },
+ {
+ "x": 513,
+ "y": 250
+ },
+ {
+ "x": 499.0480041503906,
+ "y": 249.51199340820312
+ },
+ {
+ "x": 485.1650085449219,
+ "y": 248.05299377441406
+ },
+ {
+ "x": 471.4169921875,
+ "y": 245.62899780273438
+ },
+ {
+ "x": 457.87200927734375,
+ "y": 242.2519989013672
+ },
+ {
+ "x": 444.5950012207031,
+ "y": 237.93800354003906
+ },
+ {
+ "x": 431.6520080566406,
+ "y": 232.70899963378906
+ },
+ {
+ "x": 419.1050109863281,
+ "y": 226.58900451660156
+ },
+ {
+ "x": 407.0159912109375,
+ "y": 219.60899353027344
+ },
+ {
+ "x": 395.4419860839844,
+ "y": 211.80299377441406
+ },
+ {
+ "x": 384.4419860839844,
+ "y": 203.20799255371094
+ },
+ {
+ "x": 374.0679931640625,
+ "y": 193.86700439453125
+ },
+ {
+ "x": 364.3710021972656,
+ "y": 183.8260040283203
+ },
+ {
+ "x": 355.3970031738281,
+ "y": 173.1320037841797
+ },
+ {
+ "x": 347.1919860839844,
+ "y": 161.83799743652344
+ },
+ {
+ "x": 339.79400634765625,
+ "y": 150
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "3.(a -> b)[0]",
+ "src": "3.a",
+ "srcArrow": "none",
+ "dst": "3.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,
+ "link": "",
+ "route": [
+ {
+ "x": 972,
+ "y": -200
+ },
+ {
+ "x": 992.905029296875,
+ "y": -198.9040069580078
+ },
+ {
+ "x": 1013.5819702148438,
+ "y": -195.62899780273438
+ },
+ {
+ "x": 1033.802978515625,
+ "y": -190.21099853515625
+ },
+ {
+ "x": 1053.3470458984375,
+ "y": -182.70899963378906
+ },
+ {
+ "x": 1072,
+ "y": -173.2050018310547
+ },
+ {
+ "x": 1089.5570068359375,
+ "y": -161.80299377441406
+ },
+ {
+ "x": 1105.8260498046875,
+ "y": -148.6280059814453
+ },
+ {
+ "x": 1120.6280517578125,
+ "y": -133.8260040283203
+ },
+ {
+ "x": 1133.802978515625,
+ "y": -117.55699920654297
+ },
+ {
+ "x": 1145.2049560546875,
+ "y": -100
+ },
+ {
+ "x": 1154.708984375,
+ "y": -81.34700012207031
+ },
+ {
+ "x": 1162.2110595703125,
+ "y": -61.803001403808594
+ },
+ {
+ "x": 1167.6290283203125,
+ "y": -41.582000732421875
+ },
+ {
+ "x": 1170.904052734375,
+ "y": -20.905000686645508
+ },
+ {
+ "x": 1172,
+ "y": 0
+ },
+ {
+ "x": 1170.904052734375,
+ "y": 20.905000686645508
+ },
+ {
+ "x": 1167.6290283203125,
+ "y": 41.582000732421875
+ },
+ {
+ "x": 1162.2110595703125,
+ "y": 61.803001403808594
+ },
+ {
+ "x": 1154.708984375,
+ "y": 81.34700012207031
+ },
+ {
+ "x": 1145.2049560546875,
+ "y": 99.9990005493164
+ },
+ {
+ "x": 1133.802978515625,
+ "y": 117.55699920654297
+ },
+ {
+ "x": 1120.6280517578125,
+ "y": 133.8260040283203
+ },
+ {
+ "x": 1105.8260498046875,
+ "y": 148.6280059814453
+ },
+ {
+ "x": 1089.5570068359375,
+ "y": 161.80299377441406
+ },
+ {
+ "x": 1072,
+ "y": 173.2050018310547
+ },
+ {
+ "x": 1053.3470458984375,
+ "y": 182.70899963378906
+ },
+ {
+ "x": 1033.802978515625,
+ "y": 190.21099853515625
+ },
+ {
+ "x": 1013.5819702148438,
+ "y": 195.62899780273438
+ },
+ {
+ "x": 992.905029296875,
+ "y": 198.9040069580078
+ },
+ {
+ "x": 972,
+ "y": 200
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "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": "",
+ "animated": false,
+ "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/cycle-diagram/dagre/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
new file mode 100644
index 0000000000..19f6ccf3d4
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
@@ -0,0 +1,103 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
new file mode 100644
index 0000000000..145a26b3b4
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
@@ -0,0 +1,1495 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "1",
+ "type": "cycle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 454,
+ "height": 466,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "1.a",
+ "type": "rectangle",
+ "pos": {
+ "x": -14,
+ "y": -221
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 185,
+ "y": -21
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.c",
+ "type": "rectangle",
+ "pos": {
+ "x": -14,
+ "y": 179
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "1.d",
+ "type": "rectangle",
+ "pos": {
+ "x": -215,
+ "y": -20
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2",
+ "type": "cycle",
+ "pos": {
+ "x": 485,
+ "y": 61
+ },
+ "width": 400,
+ "height": 367,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "2.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 459,
+ "y": -171
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 632,
+ "y": 128
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "2.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 285,
+ "y": 129
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "3",
+ "type": "cycle",
+ "pos": {
+ "x": 904,
+ "y": 12
+ },
+ "width": 53,
+ "height": 466,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "3.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 878,
+ "y": -221
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ },
+ {
+ "id": "3.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 878,
+ "y": 179
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "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": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "1.(a -> b)[0]",
+ "src": "1.a",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 12,
+ "y": -188
+ },
+ {
+ "x": 22.466999053955078,
+ "y": -187.72500610351562
+ },
+ {
+ "x": 32.904998779296875,
+ "y": -186.9040069580078
+ },
+ {
+ "x": 43.2859992980957,
+ "y": -185.53700256347656
+ },
+ {
+ "x": 53.582000732421875,
+ "y": -183.62899780273438
+ },
+ {
+ "x": 63.76300048828125,
+ "y": -181.18499755859375
+ },
+ {
+ "x": 73.8030014038086,
+ "y": -178.21099853515625
+ },
+ {
+ "x": 83.6729965209961,
+ "y": -174.71600341796875
+ },
+ {
+ "x": 93.34700012207031,
+ "y": -170.70899963378906
+ },
+ {
+ "x": 102.7979965209961,
+ "y": -166.2010040283203
+ },
+ {
+ "x": 112,
+ "y": -161.2050018310547
+ },
+ {
+ "x": 120.927001953125,
+ "y": -155.73399353027344
+ },
+ {
+ "x": 129.5570068359375,
+ "y": -149.80299377441406
+ },
+ {
+ "x": 137.86399841308594,
+ "y": -143.4290008544922
+ },
+ {
+ "x": 145.8260040283203,
+ "y": -136.6280059814453
+ },
+ {
+ "x": 153.42100524902344,
+ "y": -129.42100524902344
+ },
+ {
+ "x": 160.6280059814453,
+ "y": -121.82599639892578
+ },
+ {
+ "x": 167.4290008544922,
+ "y": -113.86399841308594
+ },
+ {
+ "x": 173.80299377441406,
+ "y": -105.55699920654297
+ },
+ {
+ "x": 179.73399353027344,
+ "y": -96.927001953125
+ },
+ {
+ "x": 185.2050018310547,
+ "y": -88
+ },
+ {
+ "x": 190.2010040283203,
+ "y": -78.7979965209961
+ },
+ {
+ "x": 194.70899963378906,
+ "y": -69.34700012207031
+ },
+ {
+ "x": 198.71600341796875,
+ "y": -59.67300033569336
+ },
+ {
+ "x": 202.21099853515625,
+ "y": -49.803001403808594
+ },
+ {
+ "x": 205.18499755859375,
+ "y": -39.76300048828125
+ },
+ {
+ "x": 207.62899780273438,
+ "y": -29.582000732421875
+ },
+ {
+ "x": 209.53700256347656,
+ "y": -19.285999298095703
+ },
+ {
+ "x": 210.9040069580078,
+ "y": -8.904999732971191
+ },
+ {
+ "x": 211.72500610351562,
+ "y": 1.531999945640564
+ },
+ {
+ "x": 212,
+ "y": 12
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "1.(b -> c)[0]",
+ "src": "1.b",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 212,
+ "y": 12
+ },
+ {
+ "x": 211.72500610351562,
+ "y": 22.466999053955078
+ },
+ {
+ "x": 210.9040069580078,
+ "y": 32.904998779296875
+ },
+ {
+ "x": 209.53700256347656,
+ "y": 43.2859992980957
+ },
+ {
+ "x": 207.62899780273438,
+ "y": 53.582000732421875
+ },
+ {
+ "x": 205.18499755859375,
+ "y": 63.76300048828125
+ },
+ {
+ "x": 202.21099853515625,
+ "y": 73.8030014038086
+ },
+ {
+ "x": 198.71600341796875,
+ "y": 83.6729965209961
+ },
+ {
+ "x": 194.70899963378906,
+ "y": 93.34700012207031
+ },
+ {
+ "x": 190.2010040283203,
+ "y": 102.7979965209961
+ },
+ {
+ "x": 185.2050018310547,
+ "y": 111.9990005493164
+ },
+ {
+ "x": 179.73399353027344,
+ "y": 120.927001953125
+ },
+ {
+ "x": 173.80299377441406,
+ "y": 129.5570068359375
+ },
+ {
+ "x": 167.4290008544922,
+ "y": 137.86399841308594
+ },
+ {
+ "x": 160.6280059814453,
+ "y": 145.8260040283203
+ },
+ {
+ "x": 153.42100524902344,
+ "y": 153.42100524902344
+ },
+ {
+ "x": 145.8260040283203,
+ "y": 160.6280059814453
+ },
+ {
+ "x": 137.86399841308594,
+ "y": 167.4290008544922
+ },
+ {
+ "x": 129.5570068359375,
+ "y": 173.80299377441406
+ },
+ {
+ "x": 120.927001953125,
+ "y": 179.73399353027344
+ },
+ {
+ "x": 112,
+ "y": 185.2050018310547
+ },
+ {
+ "x": 102.7979965209961,
+ "y": 190.2010040283203
+ },
+ {
+ "x": 93.34700012207031,
+ "y": 194.70899963378906
+ },
+ {
+ "x": 83.6729965209961,
+ "y": 198.71600341796875
+ },
+ {
+ "x": 73.8030014038086,
+ "y": 202.21099853515625
+ },
+ {
+ "x": 63.76300048828125,
+ "y": 205.18499755859375
+ },
+ {
+ "x": 53.582000732421875,
+ "y": 207.62899780273438
+ },
+ {
+ "x": 43.2859992980957,
+ "y": 209.53700256347656
+ },
+ {
+ "x": 32.904998779296875,
+ "y": 210.9040069580078
+ },
+ {
+ "x": 22.466999053955078,
+ "y": 211.72500610351562
+ },
+ {
+ "x": 12,
+ "y": 212
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "1.(c -> d)[0]",
+ "src": "1.c",
+ "srcArrow": "none",
+ "dst": "1.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,
+ "link": "",
+ "route": [
+ {
+ "x": 12,
+ "y": 212
+ },
+ {
+ "x": 1.531999945640564,
+ "y": 211.72500610351562
+ },
+ {
+ "x": -8.904999732971191,
+ "y": 210.9040069580078
+ },
+ {
+ "x": -19.285999298095703,
+ "y": 209.53700256347656
+ },
+ {
+ "x": -29.582000732421875,
+ "y": 207.62899780273438
+ },
+ {
+ "x": -39.76300048828125,
+ "y": 205.18499755859375
+ },
+ {
+ "x": -49.803001403808594,
+ "y": 202.21099853515625
+ },
+ {
+ "x": -59.67300033569336,
+ "y": 198.71600341796875
+ },
+ {
+ "x": -69.34700012207031,
+ "y": 194.70899963378906
+ },
+ {
+ "x": -78.7979965209961,
+ "y": 190.2010040283203
+ },
+ {
+ "x": -87.9990005493164,
+ "y": 185.2050018310547
+ },
+ {
+ "x": -96.927001953125,
+ "y": 179.73399353027344
+ },
+ {
+ "x": -105.55699920654297,
+ "y": 173.80299377441406
+ },
+ {
+ "x": -113.86399841308594,
+ "y": 167.4290008544922
+ },
+ {
+ "x": -121.82599639892578,
+ "y": 160.6280059814453
+ },
+ {
+ "x": -129.42100524902344,
+ "y": 153.42100524902344
+ },
+ {
+ "x": -136.6280059814453,
+ "y": 145.8260040283203
+ },
+ {
+ "x": -143.4290008544922,
+ "y": 137.86399841308594
+ },
+ {
+ "x": -149.80299377441406,
+ "y": 129.5570068359375
+ },
+ {
+ "x": -155.73399353027344,
+ "y": 120.927001953125
+ },
+ {
+ "x": -161.2050018310547,
+ "y": 111.9990005493164
+ },
+ {
+ "x": -166.2010040283203,
+ "y": 102.7979965209961
+ },
+ {
+ "x": -170.70899963378906,
+ "y": 93.34700012207031
+ },
+ {
+ "x": -174.71600341796875,
+ "y": 83.6729965209961
+ },
+ {
+ "x": -178.21099853515625,
+ "y": 73.8030014038086
+ },
+ {
+ "x": -181.18499755859375,
+ "y": 63.76300048828125
+ },
+ {
+ "x": -183.62899780273438,
+ "y": 53.582000732421875
+ },
+ {
+ "x": -185.53700256347656,
+ "y": 43.2859992980957
+ },
+ {
+ "x": -186.9040069580078,
+ "y": 32.904998779296875
+ },
+ {
+ "x": -187.72500610351562,
+ "y": 22.466999053955078
+ },
+ {
+ "x": -188,
+ "y": 12
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "2.(a -> b)[0]",
+ "src": "2.a",
+ "srcArrow": "none",
+ "dst": "2.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,
+ "link": "",
+ "route": [
+ {
+ "x": 485.5,
+ "y": -138
+ },
+ {
+ "x": 499.45098876953125,
+ "y": -137.51199340820312
+ },
+ {
+ "x": 513.333984375,
+ "y": -136.05299377441406
+ },
+ {
+ "x": 527.0819702148438,
+ "y": -133.62899780273438
+ },
+ {
+ "x": 540.6270141601562,
+ "y": -130.2519989013672
+ },
+ {
+ "x": 553.9039916992188,
+ "y": -125.93800354003906
+ },
+ {
+ "x": 566.8469848632812,
+ "y": -120.70899963378906
+ },
+ {
+ "x": 579.3939819335938,
+ "y": -114.58899688720703
+ },
+ {
+ "x": 591.4829711914062,
+ "y": -107.60900115966797
+ },
+ {
+ "x": 603.0570068359375,
+ "y": -99.8030014038086
+ },
+ {
+ "x": 614.0570068359375,
+ "y": -91.20800018310547
+ },
+ {
+ "x": 624.4310302734375,
+ "y": -81.86699676513672
+ },
+ {
+ "x": 634.1279907226562,
+ "y": -71.82599639892578
+ },
+ {
+ "x": 643.1019897460938,
+ "y": -61.13199996948242
+ },
+ {
+ "x": 651.3070068359375,
+ "y": -49.8380012512207
+ },
+ {
+ "x": 658.7050170898438,
+ "y": -38
+ },
+ {
+ "x": 665.2579956054688,
+ "y": -25.673999786376953
+ },
+ {
+ "x": 670.9359741210938,
+ "y": -12.920999526977539
+ },
+ {
+ "x": 675.7109985351562,
+ "y": 0.19599999487400055
+ },
+ {
+ "x": 679.5590209960938,
+ "y": 13.614999771118164
+ },
+ {
+ "x": 682.4609985351562,
+ "y": 27.270000457763672
+ },
+ {
+ "x": 684.4039916992188,
+ "y": 41.09400177001953
+ },
+ {
+ "x": 685.3779907226562,
+ "y": 55.02000045776367
+ },
+ {
+ "x": 685.3779907226562,
+ "y": 68.97899627685547
+ },
+ {
+ "x": 684.4039916992188,
+ "y": 82.90499877929688
+ },
+ {
+ "x": 682.4609985351562,
+ "y": 96.72899627685547
+ },
+ {
+ "x": 679.5590209960938,
+ "y": 110.38400268554688
+ },
+ {
+ "x": 675.7109985351562,
+ "y": 123.8030014038086
+ },
+ {
+ "x": 670.9359741210938,
+ "y": 136.92100524902344
+ },
+ {
+ "x": 665.2579956054688,
+ "y": 149.6739959716797
+ },
+ {
+ "x": 658.7050170898438,
+ "y": 161.99899291992188
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "2.(b -> c)[0]",
+ "src": "2.b",
+ "srcArrow": "none",
+ "dst": "2.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,
+ "link": "",
+ "route": [
+ {
+ "x": 658.7050170898438,
+ "y": 161.99899291992188
+ },
+ {
+ "x": 651.3070068359375,
+ "y": 173.83799743652344
+ },
+ {
+ "x": 643.1019897460938,
+ "y": 185.1320037841797
+ },
+ {
+ "x": 634.1279907226562,
+ "y": 195.8260040283203
+ },
+ {
+ "x": 624.4310302734375,
+ "y": 205.86700439453125
+ },
+ {
+ "x": 614.0570068359375,
+ "y": 215.20799255371094
+ },
+ {
+ "x": 603.0570068359375,
+ "y": 223.80299377441406
+ },
+ {
+ "x": 591.4829711914062,
+ "y": 231.60899353027344
+ },
+ {
+ "x": 579.3939819335938,
+ "y": 238.58900451660156
+ },
+ {
+ "x": 566.8469848632812,
+ "y": 244.70899963378906
+ },
+ {
+ "x": 553.9039916992188,
+ "y": 249.93800354003906
+ },
+ {
+ "x": 540.6270141601562,
+ "y": 254.2519989013672
+ },
+ {
+ "x": 527.0819702148438,
+ "y": 257.6289978027344
+ },
+ {
+ "x": 513.333984375,
+ "y": 260.0530090332031
+ },
+ {
+ "x": 499.45098876953125,
+ "y": 261.5119934082031
+ },
+ {
+ "x": 485.5,
+ "y": 262
+ },
+ {
+ "x": 471.5480041503906,
+ "y": 261.5119934082031
+ },
+ {
+ "x": 457.6650085449219,
+ "y": 260.0530090332031
+ },
+ {
+ "x": 443.9169921875,
+ "y": 257.6289978027344
+ },
+ {
+ "x": 430.37200927734375,
+ "y": 254.2519989013672
+ },
+ {
+ "x": 417.0950012207031,
+ "y": 249.93800354003906
+ },
+ {
+ "x": 404.1520080566406,
+ "y": 244.70899963378906
+ },
+ {
+ "x": 391.6050109863281,
+ "y": 238.58900451660156
+ },
+ {
+ "x": 379.5159912109375,
+ "y": 231.60899353027344
+ },
+ {
+ "x": 367.9419860839844,
+ "y": 223.80299377441406
+ },
+ {
+ "x": 356.9419860839844,
+ "y": 215.20799255371094
+ },
+ {
+ "x": 346.5679931640625,
+ "y": 205.86700439453125
+ },
+ {
+ "x": 336.8710021972656,
+ "y": 195.8260040283203
+ },
+ {
+ "x": 327.8970031738281,
+ "y": 185.1320037841797
+ },
+ {
+ "x": 319.6919860839844,
+ "y": 173.83799743652344
+ },
+ {
+ "x": 312.29400634765625,
+ "y": 162
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "3.(a -> b)[0]",
+ "src": "3.a",
+ "srcArrow": "none",
+ "dst": "3.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,
+ "link": "",
+ "route": [
+ {
+ "x": 904.9099731445312,
+ "y": -188
+ },
+ {
+ "x": 925.8150024414062,
+ "y": -186.9040069580078
+ },
+ {
+ "x": 946.4920043945312,
+ "y": -183.62899780273438
+ },
+ {
+ "x": 966.7130126953125,
+ "y": -178.21099853515625
+ },
+ {
+ "x": 986.2570190429688,
+ "y": -170.70899963378906
+ },
+ {
+ "x": 1004.9099731445312,
+ "y": -161.2050018310547
+ },
+ {
+ "x": 1022.4669799804688,
+ "y": -149.80299377441406
+ },
+ {
+ "x": 1038.7359619140625,
+ "y": -136.6280059814453
+ },
+ {
+ "x": 1053.5389404296875,
+ "y": -121.82599639892578
+ },
+ {
+ "x": 1066.7130126953125,
+ "y": -105.55699920654297
+ },
+ {
+ "x": 1078.114990234375,
+ "y": -88
+ },
+ {
+ "x": 1087.6190185546875,
+ "y": -69.34700012207031
+ },
+ {
+ "x": 1095.1209716796875,
+ "y": -49.803001403808594
+ },
+ {
+ "x": 1100.5389404296875,
+ "y": -29.582000732421875
+ },
+ {
+ "x": 1103.81396484375,
+ "y": -8.904999732971191
+ },
+ {
+ "x": 1104.9100341796875,
+ "y": 12
+ },
+ {
+ "x": 1103.81396484375,
+ "y": 32.904998779296875
+ },
+ {
+ "x": 1100.5389404296875,
+ "y": 53.582000732421875
+ },
+ {
+ "x": 1095.1209716796875,
+ "y": 73.8030014038086
+ },
+ {
+ "x": 1087.6190185546875,
+ "y": 93.34700012207031
+ },
+ {
+ "x": 1078.114990234375,
+ "y": 111.9990005493164
+ },
+ {
+ "x": 1066.7130126953125,
+ "y": 129.5570068359375
+ },
+ {
+ "x": 1053.5389404296875,
+ "y": 145.8260040283203
+ },
+ {
+ "x": 1038.7359619140625,
+ "y": 160.6280059814453
+ },
+ {
+ "x": 1022.4669799804688,
+ "y": 173.80299377441406
+ },
+ {
+ "x": 1004.9099731445312,
+ "y": 185.2050018310547
+ },
+ {
+ "x": 986.2570190429688,
+ "y": 194.70899963378906
+ },
+ {
+ "x": 966.7130126953125,
+ "y": 202.21099853515625
+ },
+ {
+ "x": 946.4920043945312,
+ "y": 207.62899780273438
+ },
+ {
+ "x": 925.8150024414062,
+ "y": 210.9040069580078
+ },
+ {
+ "x": 904.9099731445312,
+ "y": 212
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "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": "",
+ "animated": false,
+ "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/cycle-diagram/elk/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
new file mode 100644
index 0000000000..19ee385912
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
@@ -0,0 +1,103 @@
+abcdabcab
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index fcc6a76192..b1c425fc29 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -775,3 +775,17 @@ a -> b: hello {
b -> c: {
icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
}
+
+-- cycle-diagram --
+1: "" {
+ shape: cycle
+ a -> b -> c -> d
+}
+2: "" {
+ shape: cycle
+ a -> b -> c
+}
+3: "" {
+ shape: cycle
+ a -> b
+}
diff --git a/lib/geo/point.go b/lib/geo/point.go
index ab8e034a02..4fb1082396 100644
--- a/lib/geo/point.go
+++ b/lib/geo/point.go
@@ -324,3 +324,12 @@ func RemovePoints(points Points, toRemove []bool) Points {
}
return without
}
+
+func (v Vector) Normalize() Vector {
+ length := v.Length()
+ if length == 0 {
+ // avoid dividing by 0
+ return Vector{0, 0}
+ }
+ return Vector{v[0] / length, v[1] / length}
+}