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..d3d89cc1b4
--- /dev/null
+++ b/d2layouts/d2cycle/layout.go
@@ -0,0 +1,259 @@
+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/d2/lib/shape"
+ "oss.terrastruct.com/util-go/go2"
+)
+
+const (
+ MIN_RADIUS = 200
+ PADDING = 20
+ ARC_STEPS = 30
+)
+
+// Layout arranges nodes in a circle and routes each edge as a circular arc
+// that starts and ends on the borders of its source and destination shapes.
+func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
+ objects := g.Root.ChildrenArray
+ if len(objects) == 0 {
+ return nil
+ }
+
+ for _, obj := range g.Objects {
+ positionLabelsIcons(obj)
+ }
+
+ radius := calculateRadius(objects)
+ positionObjects(objects, radius)
+
+ for _, edge := range g.Edges {
+ createCircularArc(edge, radius)
+ }
+
+ 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)
+ }
+ 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))
+ 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)
+
+ obj.TopLeft = geo.NewPoint(
+ x-obj.Box.Width/2,
+ y-obj.Box.Height/2,
+ )
+ }
+}
+
+// createCircularArc routes a single edge as a circular arc whose endpoints
+// lie exactly on the borders of the source and destination shapes. The arc
+// belongs to the layout circle, centered at the origin with the given radius;
+// the source and destination shape centers both lie on that circle.
+func createCircularArc(edge *d2graph.Edge, radius float64) {
+ if edge.Src == nil || edge.Dst == nil {
+ return
+ }
+
+ srcCenter := edge.Src.Center()
+ dstCenter := edge.Dst.Center()
+ origin := geo.NewPoint(0, 0)
+
+ srcAngle := math.Atan2(srcCenter.Y, srcCenter.X)
+ dstAngle := math.Atan2(dstCenter.Y, dstCenter.X)
+ if dstAngle < srcAngle {
+ dstAngle += 2 * math.Pi
+ }
+ sweep := dstAngle - srcAngle
+ srcShape := edge.Src.ToShape()
+ dstShape := edge.Dst.ToShape()
+ if sweep <= 0 {
+ fallbackStraightRoute(edge, srcShape, dstShape, srcCenter, dstCenter)
+ return
+ }
+
+ startAngle, hasStart := nextBoundaryAngle(edge.Src.Box, origin, radius, srcAngle, sweep, true)
+ endAngle, hasEnd := nextBoundaryAngle(edge.Dst.Box, origin, radius, srcAngle, sweep, false)
+ if !hasStart || !hasEnd || endAngle <= startAngle {
+ fallbackStraightRoute(edge, srcShape, dstShape, srcCenter, dstCenter)
+ return
+ }
+
+ path := make([]*geo.Point, 0, ARC_STEPS+1)
+ for i := 0; i <= ARC_STEPS; i++ {
+ t := float64(i) / float64(ARC_STEPS)
+ angle := startAngle + t*(endAngle-startAngle)
+ path = append(path, geo.NewPoint(radius*math.Cos(angle), radius*math.Sin(angle)))
+ }
+
+ // path[0] / path[len-1] sit on the bounding-box border of the source /
+ // destination shape. For non-rectangular shapes (circle, oval, hexagon,
+ // cloud, ...) the bounding box border is not the shape border, so trace
+ // each endpoint inward from the shape center to the actual shape outline.
+ // TraceToShapeBorder is a no-op for rectangular shapes.
+ path[0] = shape.TraceToShapeBorder(srcShape, path[0], srcCenter)
+ path[len(path)-1] = shape.TraceToShapeBorder(dstShape, path[len(path)-1], dstCenter)
+
+ edge.Route = path
+ edge.IsCurve = true
+}
+
+// fallbackStraightRoute renders a straight connection whose endpoints are
+// clipped to the source and destination shape borders along the line between
+// the two centers, used when the analytic arc geometry degenerates (zero
+// sweep, no boundary crossing in the arc range, etc.).
+func fallbackStraightRoute(edge *d2graph.Edge, srcShape, dstShape shape.Shape, srcCenter, dstCenter *geo.Point) {
+ srcBorder := clipToShapeBorder(srcShape, edge.Src.Box, srcCenter, dstCenter)
+ dstBorder := clipToShapeBorder(dstShape, edge.Dst.Box, dstCenter, srcCenter)
+ edge.Route = []*geo.Point{srcBorder, dstBorder}
+ edge.IsCurve = false
+}
+
+// clipToShapeBorder returns the point where a ray from `from` (assumed inside
+// `box`) toward `toward` first exits the actual shape outline. The bounding
+// box is consulted first to obtain a rectangular border point, then the shape
+// helper refines it for non-rectangular shapes.
+func clipToShapeBorder(shp shape.Shape, box *geo.Box, from, toward *geo.Point) *geo.Point {
+ dx := toward.X - from.X
+ dy := toward.Y - from.Y
+ dist := math.Hypot(dx, dy)
+ if dist == 0 {
+ return from
+ }
+ // Extend the ray well past `toward` so the segment definitely exits the
+ // box even when `toward` itself sits inside the box.
+ diag := math.Hypot(box.Width, box.Height)
+ scale := (dist + 2*diag) / dist
+ extended := geo.NewPoint(from.X+dx*scale, from.Y+dy*scale)
+
+ rectBorder := extended
+ if pts := box.Intersections(geo.Segment{Start: from, End: extended}); len(pts) > 0 {
+ rectBorder = pts[0]
+ }
+ return shape.TraceToShapeBorder(shp, rectBorder, from)
+}
+
+// nextBoundaryAngle scans the angles where the layout circle crosses an edge
+// of the box. When forSrc is true it returns the smallest such angle strictly
+// greater than srcAngle (the point where the arc exits the source box). When
+// forSrc is false it returns the largest such angle strictly less than
+// srcAngle+sweep (the point where the arc enters the destination box). The
+// boolean is false when no crossing exists in the (srcAngle, srcAngle+sweep)
+// range, in which case the caller falls back to the shape center.
+func nextBoundaryAngle(box *geo.Box, origin *geo.Point, radius, srcAngle, sweep float64, forSrc bool) (float64, bool) {
+ candidates := boxCircleIntersectionAngles(box, origin, radius)
+ endAngle := srcAngle + sweep
+
+ var best float64
+ found := false
+ for _, raw := range candidates {
+ a := raw
+ for a <= srcAngle {
+ a += 2 * math.Pi
+ }
+ for a > srcAngle+2*math.Pi {
+ a -= 2 * math.Pi
+ }
+ if a >= endAngle {
+ continue
+ }
+ if !found {
+ best = a
+ found = true
+ continue
+ }
+ if forSrc {
+ if a < best {
+ best = a
+ }
+ } else {
+ if a > best {
+ best = a
+ }
+ }
+ }
+ return best, found
+}
+
+func boxCircleIntersectionAngles(box *geo.Box, origin *geo.Point, radius float64) []float64 {
+ edges := boxEdges(box)
+ var angles []float64
+ for _, e := range edges {
+ for _, p := range e.IntersectCircle(origin, radius) {
+ angles = append(angles, math.Atan2(p.Y-origin.Y, p.X-origin.X))
+ }
+ }
+ return angles
+}
+
+func boxEdges(box *geo.Box) []geo.Segment {
+ tl := box.TopLeft
+ tr := geo.NewPoint(tl.X+box.Width, tl.Y)
+ bl := geo.NewPoint(tl.X, tl.Y+box.Height)
+ br := geo.NewPoint(tl.X+box.Width, tl.Y+box.Height)
+ return []geo.Segment{
+ {Start: tl, End: tr},
+ {Start: tr, End: br},
+ {Start: br, End: bl},
+ {Start: bl, End: tl},
+ }
+}
+
+// positionLabelsIcons applies a sensible default label/icon position when one
+// has not been explicitly specified.
+func positionLabelsIcons(obj *d2graph.Object) {
+ 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 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 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 c0d41e3973..de1c5a9da0 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)
@@ -364,6 +371,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 4001a853bd..0277fc33a3 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -834,37 +834,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)
@@ -872,20 +913,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,
@@ -893,7 +940,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,
))
@@ -907,11 +954,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 63fcfacbf3..08f130c803 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -1072,6 +1072,7 @@ const (
ShapeSQLTable = "sql_table"
ShapeImage = "image"
ShapeSequenceDiagram = "sequence_diagram"
+ ShapeCycleDiagram = "cycle"
ShapeHierarchy = "hierarchy"
)
@@ -1100,6 +1101,7 @@ var Shapes = []string{
ShapeSQLTable,
ShapeImage,
ShapeSequenceDiagram,
+ ShapeCycleDiagram,
ShapeHierarchy,
}
@@ -1170,6 +1172,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..a74da10e86
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
@@ -0,0 +1,1496 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "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": 26.5,
+ "y": -198.23599243164062
+ },
+ {
+ "x": 34.87900161743164,
+ "y": -196.93499755859375
+ },
+ {
+ "x": 43.196998596191406,
+ "y": -195.2790069580078
+ },
+ {
+ "x": 51.43600082397461,
+ "y": -193.27200317382812
+ },
+ {
+ "x": 59.58300018310547,
+ "y": -190.91799926757812
+ },
+ {
+ "x": 67.62300109863281,
+ "y": -188.22000122070312
+ },
+ {
+ "x": 75.54100036621094,
+ "y": -185.1840057373047
+ },
+ {
+ "x": 83.3239974975586,
+ "y": -181.8159942626953
+ },
+ {
+ "x": 90.95600128173828,
+ "y": -178.1199951171875
+ },
+ {
+ "x": 98.4260025024414,
+ "y": -174.10400390625
+ },
+ {
+ "x": 105.71800231933594,
+ "y": -169.77499389648438
+ },
+ {
+ "x": 112.81999969482422,
+ "y": -165.14100646972656
+ },
+ {
+ "x": 119.71900177001953,
+ "y": -160.20899963378906
+ },
+ {
+ "x": 126.40299987792969,
+ "y": -154.99000549316406
+ },
+ {
+ "x": 132.86000061035156,
+ "y": -149.49200439453125
+ },
+ {
+ "x": 139.0780029296875,
+ "y": -143.7259979248047
+ },
+ {
+ "x": 145.04600524902344,
+ "y": -137.7010040283203
+ },
+ {
+ "x": 150.7530059814453,
+ "y": -131.42799377441406
+ },
+ {
+ "x": 156.18899536132812,
+ "y": -124.91899871826172
+ },
+ {
+ "x": 161.343994140625,
+ "y": -118.18599700927734
+ },
+ {
+ "x": 166.20899963378906,
+ "y": -111.23999786376953
+ },
+ {
+ "x": 170.77499389648438,
+ "y": -104.09400177001953
+ },
+ {
+ "x": 175.03500366210938,
+ "y": -96.76100158691406
+ },
+ {
+ "x": 178.97900390625,
+ "y": -89.25299835205078
+ },
+ {
+ "x": 182.6020050048828,
+ "y": -81.58599853515625
+ },
+ {
+ "x": 185.89599609375,
+ "y": -73.77200317382812
+ },
+ {
+ "x": 188.85699462890625,
+ "y": -65.82499694824219
+ },
+ {
+ "x": 191.4770050048828,
+ "y": -57.75899887084961
+ },
+ {
+ "x": 193.75399780273438,
+ "y": -49.59000015258789
+ },
+ {
+ "x": 195.6820068359375,
+ "y": -41.332000732421875
+ },
+ {
+ "x": 197.25799560546875,
+ "y": -33
+ }
+ ],
+ "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": 197.25799560546875,
+ "y": 32.999000549316406
+ },
+ {
+ "x": 195.6820068359375,
+ "y": 41.332000732421875
+ },
+ {
+ "x": 193.75399780273438,
+ "y": 49.59000015258789
+ },
+ {
+ "x": 191.4770050048828,
+ "y": 57.75899887084961
+ },
+ {
+ "x": 188.85699462890625,
+ "y": 65.82499694824219
+ },
+ {
+ "x": 185.89599609375,
+ "y": 73.77200317382812
+ },
+ {
+ "x": 182.6020050048828,
+ "y": 81.58599853515625
+ },
+ {
+ "x": 178.97900390625,
+ "y": 89.25299835205078
+ },
+ {
+ "x": 175.03500366210938,
+ "y": 96.76100158691406
+ },
+ {
+ "x": 170.77499389648438,
+ "y": 104.09400177001953
+ },
+ {
+ "x": 166.20899963378906,
+ "y": 111.23999786376953
+ },
+ {
+ "x": 161.343994140625,
+ "y": 118.18599700927734
+ },
+ {
+ "x": 156.18899536132812,
+ "y": 124.91899871826172
+ },
+ {
+ "x": 150.7530059814453,
+ "y": 131.42799377441406
+ },
+ {
+ "x": 145.04600524902344,
+ "y": 137.7010040283203
+ },
+ {
+ "x": 139.0780029296875,
+ "y": 143.7259979248047
+ },
+ {
+ "x": 132.86000061035156,
+ "y": 149.49200439453125
+ },
+ {
+ "x": 126.40299987792969,
+ "y": 154.99000549316406
+ },
+ {
+ "x": 119.71900177001953,
+ "y": 160.20899963378906
+ },
+ {
+ "x": 112.81999969482422,
+ "y": 165.14100646972656
+ },
+ {
+ "x": 105.71800231933594,
+ "y": 169.77499389648438
+ },
+ {
+ "x": 98.4260025024414,
+ "y": 174.10400390625
+ },
+ {
+ "x": 90.95600128173828,
+ "y": 178.1199951171875
+ },
+ {
+ "x": 83.3239974975586,
+ "y": 181.8159942626953
+ },
+ {
+ "x": 75.54100036621094,
+ "y": 185.1840057373047
+ },
+ {
+ "x": 67.62300109863281,
+ "y": 188.22000122070312
+ },
+ {
+ "x": 59.58300018310547,
+ "y": 190.91799926757812
+ },
+ {
+ "x": 51.43600082397461,
+ "y": 193.27200317382812
+ },
+ {
+ "x": 43.196998596191406,
+ "y": 195.2790069580078
+ },
+ {
+ "x": 34.87900161743164,
+ "y": 196.93499755859375
+ },
+ {
+ "x": 26.5,
+ "y": 198.23599243164062
+ }
+ ],
+ "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": -26.499000549316406,
+ "y": 198.23599243164062
+ },
+ {
+ "x": -34.87900161743164,
+ "y": 196.93499755859375
+ },
+ {
+ "x": -43.196998596191406,
+ "y": 195.2790069580078
+ },
+ {
+ "x": -51.43600082397461,
+ "y": 193.27200317382812
+ },
+ {
+ "x": -59.58300018310547,
+ "y": 190.91799926757812
+ },
+ {
+ "x": -67.62300109863281,
+ "y": 188.22000122070312
+ },
+ {
+ "x": -75.54100036621094,
+ "y": 185.1840057373047
+ },
+ {
+ "x": -83.3239974975586,
+ "y": 181.8159942626953
+ },
+ {
+ "x": -90.95600128173828,
+ "y": 178.1199951171875
+ },
+ {
+ "x": -98.4260025024414,
+ "y": 174.10400390625
+ },
+ {
+ "x": -105.71800231933594,
+ "y": 169.77499389648438
+ },
+ {
+ "x": -112.81999969482422,
+ "y": 165.14100646972656
+ },
+ {
+ "x": -119.71900177001953,
+ "y": 160.20899963378906
+ },
+ {
+ "x": -126.40299987792969,
+ "y": 154.99000549316406
+ },
+ {
+ "x": -132.86000061035156,
+ "y": 149.49200439453125
+ },
+ {
+ "x": -139.0780029296875,
+ "y": 143.7259979248047
+ },
+ {
+ "x": -145.04600524902344,
+ "y": 137.7010040283203
+ },
+ {
+ "x": -150.7530059814453,
+ "y": 131.42799377441406
+ },
+ {
+ "x": -156.18899536132812,
+ "y": 124.91899871826172
+ },
+ {
+ "x": -161.343994140625,
+ "y": 118.18599700927734
+ },
+ {
+ "x": -166.20899963378906,
+ "y": 111.23999786376953
+ },
+ {
+ "x": -170.77499389648438,
+ "y": 104.09400177001953
+ },
+ {
+ "x": -175.03500366210938,
+ "y": 96.76100158691406
+ },
+ {
+ "x": -178.97900390625,
+ "y": 89.25299835205078
+ },
+ {
+ "x": -182.6020050048828,
+ "y": 81.58599853515625
+ },
+ {
+ "x": -185.89599609375,
+ "y": 73.77200317382812
+ },
+ {
+ "x": -188.85699462890625,
+ "y": 65.82499694824219
+ },
+ {
+ "x": -191.4770050048828,
+ "y": 57.75899887084961
+ },
+ {
+ "x": -193.75399780273438,
+ "y": 49.59000015258789
+ },
+ {
+ "x": -195.6820068359375,
+ "y": 41.332000732421875
+ },
+ {
+ "x": -197.25799560546875,
+ "y": 33
+ }
+ ],
+ "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": 539.5,
+ "y": -148.23599243164062
+ },
+ {
+ "x": 551.2050170898438,
+ "y": -146.3159942626953
+ },
+ {
+ "x": 562.7760009765625,
+ "y": -143.70599365234375
+ },
+ {
+ "x": 574.1719970703125,
+ "y": -140.4149932861328
+ },
+ {
+ "x": 585.3519897460938,
+ "y": -136.4530029296875
+ },
+ {
+ "x": 596.2780151367188,
+ "y": -131.83599853515625
+ },
+ {
+ "x": 606.9119873046875,
+ "y": -126.58000183105469
+ },
+ {
+ "x": 617.2150268554688,
+ "y": -120.7020034790039
+ },
+ {
+ "x": 627.1510009765625,
+ "y": -114.2229995727539
+ },
+ {
+ "x": 636.6859741210938,
+ "y": -107.16699981689453
+ },
+ {
+ "x": 645.7849731445312,
+ "y": -99.55899810791016
+ },
+ {
+ "x": 654.4180297851562,
+ "y": -91.42400360107422
+ },
+ {
+ "x": 662.552978515625,
+ "y": -82.79100036621094
+ },
+ {
+ "x": 670.1619873046875,
+ "y": -73.69200134277344
+ },
+ {
+ "x": 677.218994140625,
+ "y": -64.15699768066406
+ },
+ {
+ "x": 683.697021484375,
+ "y": -54.22100067138672
+ },
+ {
+ "x": 689.5759887695312,
+ "y": -43.91899871826172
+ },
+ {
+ "x": 694.8330078125,
+ "y": -33.2859992980957
+ },
+ {
+ "x": 699.4509887695312,
+ "y": -22.360000610351562
+ },
+ {
+ "x": 703.4119873046875,
+ "y": -11.178999900817871
+ },
+ {
+ "x": 706.7039794921875,
+ "y": 0.2150000035762787
+ },
+ {
+ "x": 709.3150024414062,
+ "y": 11.78600025177002
+ },
+ {
+ "x": 711.2349853515625,
+ "y": 23.492000579833984
+ },
+ {
+ "x": 712.4580078125,
+ "y": 35.290000915527344
+ },
+ {
+ "x": 712.97900390625,
+ "y": 47.13999938964844
+ },
+ {
+ "x": 712.7969970703125,
+ "y": 59
+ },
+ {
+ "x": 711.9119873046875,
+ "y": 70.8290023803711
+ },
+ {
+ "x": 710.3270263671875,
+ "y": 82.58399963378906
+ },
+ {
+ "x": 708.0479736328125,
+ "y": 94.2249984741211
+ },
+ {
+ "x": 705.083984375,
+ "y": 105.70999908447266
+ },
+ {
+ "x": 701.4429931640625,
+ "y": 116.9990005493164
+ }
+ ],
+ "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": 662.3679809570312,
+ "y": 182.99899291992188
+ },
+ {
+ "x": 654.6589965820312,
+ "y": 191.1820068359375
+ },
+ {
+ "x": 646.5020141601562,
+ "y": 198.91900634765625
+ },
+ {
+ "x": 637.9229736328125,
+ "y": 206.18600463867188
+ },
+ {
+ "x": 628.9500122070312,
+ "y": 212.95799255371094
+ },
+ {
+ "x": 619.6099853515625,
+ "y": 219.21600341796875
+ },
+ {
+ "x": 609.9329833984375,
+ "y": 224.93899536132812
+ },
+ {
+ "x": 599.9500122070312,
+ "y": 230.11000061035156
+ },
+ {
+ "x": 589.6920166015625,
+ "y": 234.71099853515625
+ },
+ {
+ "x": 579.1920166015625,
+ "y": 238.72799682617188
+ },
+ {
+ "x": 568.4819946289062,
+ "y": 242.14999389648438
+ },
+ {
+ "x": 557.5980224609375,
+ "y": 244.96400451660156
+ },
+ {
+ "x": 546.572021484375,
+ "y": 247.16200256347656
+ },
+ {
+ "x": 535.4400024414062,
+ "y": 248.73699951171875
+ },
+ {
+ "x": 524.2379760742188,
+ "y": 249.6840057373047
+ },
+ {
+ "x": 513,
+ "y": 250
+ },
+ {
+ "x": 501.760986328125,
+ "y": 249.6840057373047
+ },
+ {
+ "x": 490.5589904785156,
+ "y": 248.73699951171875
+ },
+ {
+ "x": 479.427001953125,
+ "y": 247.16200256347656
+ },
+ {
+ "x": 468.4010009765625,
+ "y": 244.96400451660156
+ },
+ {
+ "x": 457.5169982910156,
+ "y": 242.14999389648438
+ },
+ {
+ "x": 446.8070068359375,
+ "y": 238.72799682617188
+ },
+ {
+ "x": 436.3070068359375,
+ "y": 234.71099853515625
+ },
+ {
+ "x": 426.04901123046875,
+ "y": 230.11000061035156
+ },
+ {
+ "x": 416.0660095214844,
+ "y": 224.93899536132812
+ },
+ {
+ "x": 406.3890075683594,
+ "y": 219.21600341796875
+ },
+ {
+ "x": 397.04901123046875,
+ "y": 212.95799255371094
+ },
+ {
+ "x": 388.07598876953125,
+ "y": 206.18600463867188
+ },
+ {
+ "x": 379.49700927734375,
+ "y": 198.91900634765625
+ },
+ {
+ "x": 371.3399963378906,
+ "y": 191.1820068359375
+ },
+ {
+ "x": 363.6310119628906,
+ "y": 183
+ }
+ ],
+ "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": 998.5,
+ "y": -198.23599243164062
+ },
+ {
+ "x": 1017.3519897460938,
+ "y": -194.7899932861328
+ },
+ {
+ "x": 1035.7879638671875,
+ "y": -189.5540008544922
+ },
+ {
+ "x": 1053.637939453125,
+ "y": -182.57899475097656
+ },
+ {
+ "x": 1070.738037109375,
+ "y": -173.927001953125
+ },
+ {
+ "x": 1086.9320068359375,
+ "y": -163.677001953125
+ },
+ {
+ "x": 1102.071044921875,
+ "y": -151.9250030517578
+ },
+ {
+ "x": 1116.0150146484375,
+ "y": -138.7779998779297
+ },
+ {
+ "x": 1128.636962890625,
+ "y": -124.35700225830078
+ },
+ {
+ "x": 1139.8199462890625,
+ "y": -108.79399871826172
+ },
+ {
+ "x": 1149.4630126953125,
+ "y": -92.23100280761719
+ },
+ {
+ "x": 1157.4759521484375,
+ "y": -74.8219985961914
+ },
+ {
+ "x": 1163.7860107421875,
+ "y": -56.72600173950195
+ },
+ {
+ "x": 1168.3349609375,
+ "y": -38.10900115966797
+ },
+ {
+ "x": 1171.0810546875,
+ "y": -19.142000198364258
+ },
+ {
+ "x": 1172,
+ "y": 0
+ },
+ {
+ "x": 1171.0810546875,
+ "y": 19.142000198364258
+ },
+ {
+ "x": 1168.3349609375,
+ "y": 38.10900115966797
+ },
+ {
+ "x": 1163.7860107421875,
+ "y": 56.72600173950195
+ },
+ {
+ "x": 1157.4759521484375,
+ "y": 74.8219985961914
+ },
+ {
+ "x": 1149.4630126953125,
+ "y": 92.23100280761719
+ },
+ {
+ "x": 1139.8199462890625,
+ "y": 108.79399871826172
+ },
+ {
+ "x": 1128.636962890625,
+ "y": 124.35700225830078
+ },
+ {
+ "x": 1116.0150146484375,
+ "y": 138.7779998779297
+ },
+ {
+ "x": 1102.071044921875,
+ "y": 151.9250030517578
+ },
+ {
+ "x": 1086.9320068359375,
+ "y": 163.677001953125
+ },
+ {
+ "x": 1070.738037109375,
+ "y": 173.927001953125
+ },
+ {
+ "x": 1053.637939453125,
+ "y": 182.57899475097656
+ },
+ {
+ "x": 1035.7879638671875,
+ "y": 189.5540008544922
+ },
+ {
+ "x": 1017.3519897460938,
+ "y": 194.7899932861328
+ },
+ {
+ "x": 998.5,
+ "y": 198.23599243164062
+ }
+ ],
+ "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..13128a2684
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
@@ -0,0 +1,95 @@
+
\ 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..10f5db1aaf
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
@@ -0,0 +1,1496 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "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": 38.5,
+ "y": -186.23599243164062
+ },
+ {
+ "x": 46.87900161743164,
+ "y": -184.93499755859375
+ },
+ {
+ "x": 55.196998596191406,
+ "y": -183.2790069580078
+ },
+ {
+ "x": 63.43600082397461,
+ "y": -181.27200317382812
+ },
+ {
+ "x": 71.58300018310547,
+ "y": -178.91799926757812
+ },
+ {
+ "x": 79.62300109863281,
+ "y": -176.22000122070312
+ },
+ {
+ "x": 87.54100036621094,
+ "y": -173.1840057373047
+ },
+ {
+ "x": 95.3239974975586,
+ "y": -169.8159942626953
+ },
+ {
+ "x": 102.95600128173828,
+ "y": -166.1199951171875
+ },
+ {
+ "x": 110.4260025024414,
+ "y": -162.10400390625
+ },
+ {
+ "x": 117.71800231933594,
+ "y": -157.77499389648438
+ },
+ {
+ "x": 124.81999969482422,
+ "y": -153.14100646972656
+ },
+ {
+ "x": 131.718994140625,
+ "y": -148.20899963378906
+ },
+ {
+ "x": 138.4029998779297,
+ "y": -142.99000549316406
+ },
+ {
+ "x": 144.86000061035156,
+ "y": -137.49200439453125
+ },
+ {
+ "x": 151.0780029296875,
+ "y": -131.7259979248047
+ },
+ {
+ "x": 157.04600524902344,
+ "y": -125.70099639892578
+ },
+ {
+ "x": 162.7530059814453,
+ "y": -119.4280014038086
+ },
+ {
+ "x": 168.18899536132812,
+ "y": -112.91899871826172
+ },
+ {
+ "x": 173.343994140625,
+ "y": -106.18599700927734
+ },
+ {
+ "x": 178.20899963378906,
+ "y": -99.23999786376953
+ },
+ {
+ "x": 182.77499389648438,
+ "y": -92.09400177001953
+ },
+ {
+ "x": 187.03500366210938,
+ "y": -84.76100158691406
+ },
+ {
+ "x": 190.97900390625,
+ "y": -77.25299835205078
+ },
+ {
+ "x": 194.6020050048828,
+ "y": -69.58599853515625
+ },
+ {
+ "x": 197.89599609375,
+ "y": -61.77199935913086
+ },
+ {
+ "x": 200.85699462890625,
+ "y": -53.82500076293945
+ },
+ {
+ "x": 203.4770050048828,
+ "y": -45.75899887084961
+ },
+ {
+ "x": 205.75399780273438,
+ "y": -37.59000015258789
+ },
+ {
+ "x": 207.6820068359375,
+ "y": -29.332000732421875
+ },
+ {
+ "x": 209.25799560546875,
+ "y": -21
+ }
+ ],
+ "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": 209.25799560546875,
+ "y": 44.999000549316406
+ },
+ {
+ "x": 207.6820068359375,
+ "y": 53.332000732421875
+ },
+ {
+ "x": 205.75399780273438,
+ "y": 61.59000015258789
+ },
+ {
+ "x": 203.4770050048828,
+ "y": 69.75900268554688
+ },
+ {
+ "x": 200.85699462890625,
+ "y": 77.82499694824219
+ },
+ {
+ "x": 197.89599609375,
+ "y": 85.77200317382812
+ },
+ {
+ "x": 194.6020050048828,
+ "y": 93.58599853515625
+ },
+ {
+ "x": 190.97900390625,
+ "y": 101.25299835205078
+ },
+ {
+ "x": 187.03500366210938,
+ "y": 108.76100158691406
+ },
+ {
+ "x": 182.77499389648438,
+ "y": 116.09400177001953
+ },
+ {
+ "x": 178.20899963378906,
+ "y": 123.23999786376953
+ },
+ {
+ "x": 173.343994140625,
+ "y": 130.18600463867188
+ },
+ {
+ "x": 168.18899536132812,
+ "y": 136.91900634765625
+ },
+ {
+ "x": 162.7530059814453,
+ "y": 143.42799377441406
+ },
+ {
+ "x": 157.04600524902344,
+ "y": 149.7010040283203
+ },
+ {
+ "x": 151.0780029296875,
+ "y": 155.7259979248047
+ },
+ {
+ "x": 144.86000061035156,
+ "y": 161.49200439453125
+ },
+ {
+ "x": 138.4029998779297,
+ "y": 166.99000549316406
+ },
+ {
+ "x": 131.718994140625,
+ "y": 172.20899963378906
+ },
+ {
+ "x": 124.81999969482422,
+ "y": 177.14100646972656
+ },
+ {
+ "x": 117.71800231933594,
+ "y": 181.77499389648438
+ },
+ {
+ "x": 110.4260025024414,
+ "y": 186.10400390625
+ },
+ {
+ "x": 102.95600128173828,
+ "y": 190.1199951171875
+ },
+ {
+ "x": 95.3239974975586,
+ "y": 193.8159942626953
+ },
+ {
+ "x": 87.54100036621094,
+ "y": 197.1840057373047
+ },
+ {
+ "x": 79.62300109863281,
+ "y": 200.22000122070312
+ },
+ {
+ "x": 71.58300018310547,
+ "y": 202.91799926757812
+ },
+ {
+ "x": 63.43600082397461,
+ "y": 205.27200317382812
+ },
+ {
+ "x": 55.196998596191406,
+ "y": 207.2790069580078
+ },
+ {
+ "x": 46.87900161743164,
+ "y": 208.93499755859375
+ },
+ {
+ "x": 38.5,
+ "y": 210.23599243164062
+ }
+ ],
+ "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": -14.49899959564209,
+ "y": 210.23599243164062
+ },
+ {
+ "x": -22.878999710083008,
+ "y": 208.93499755859375
+ },
+ {
+ "x": -31.19700050354004,
+ "y": 207.2790069580078
+ },
+ {
+ "x": -39.43600082397461,
+ "y": 205.27200317382812
+ },
+ {
+ "x": -47.58300018310547,
+ "y": 202.91799926757812
+ },
+ {
+ "x": -55.62300109863281,
+ "y": 200.22000122070312
+ },
+ {
+ "x": -63.54100036621094,
+ "y": 197.1840057373047
+ },
+ {
+ "x": -71.3239974975586,
+ "y": 193.8159942626953
+ },
+ {
+ "x": -78.95600128173828,
+ "y": 190.1199951171875
+ },
+ {
+ "x": -86.4260025024414,
+ "y": 186.10400390625
+ },
+ {
+ "x": -93.71800231933594,
+ "y": 181.77499389648438
+ },
+ {
+ "x": -100.81999969482422,
+ "y": 177.14100646972656
+ },
+ {
+ "x": -107.71900177001953,
+ "y": 172.20899963378906
+ },
+ {
+ "x": -114.40299987792969,
+ "y": 166.99000549316406
+ },
+ {
+ "x": -120.86000061035156,
+ "y": 161.49200439453125
+ },
+ {
+ "x": -127.0780029296875,
+ "y": 155.7259979248047
+ },
+ {
+ "x": -133.04600524902344,
+ "y": 149.7010040283203
+ },
+ {
+ "x": -138.7530059814453,
+ "y": 143.42799377441406
+ },
+ {
+ "x": -144.18899536132812,
+ "y": 136.91900634765625
+ },
+ {
+ "x": -149.343994140625,
+ "y": 130.18600463867188
+ },
+ {
+ "x": -154.20899963378906,
+ "y": 123.23999786376953
+ },
+ {
+ "x": -158.77499389648438,
+ "y": 116.09400177001953
+ },
+ {
+ "x": -163.03500366210938,
+ "y": 108.76100158691406
+ },
+ {
+ "x": -166.97900390625,
+ "y": 101.25299835205078
+ },
+ {
+ "x": -170.6020050048828,
+ "y": 93.58599853515625
+ },
+ {
+ "x": -173.89599609375,
+ "y": 85.77200317382812
+ },
+ {
+ "x": -176.85699462890625,
+ "y": 77.82499694824219
+ },
+ {
+ "x": -179.4770050048828,
+ "y": 69.75900268554688
+ },
+ {
+ "x": -181.75399780273438,
+ "y": 61.59000015258789
+ },
+ {
+ "x": -183.6820068359375,
+ "y": 53.332000732421875
+ },
+ {
+ "x": -185.25799560546875,
+ "y": 45
+ }
+ ],
+ "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": 512,
+ "y": -136.23599243164062
+ },
+ {
+ "x": 523.7050170898438,
+ "y": -134.3159942626953
+ },
+ {
+ "x": 535.2760009765625,
+ "y": -131.70599365234375
+ },
+ {
+ "x": 546.6719970703125,
+ "y": -128.4149932861328
+ },
+ {
+ "x": 557.8519897460938,
+ "y": -124.4530029296875
+ },
+ {
+ "x": 568.7780151367188,
+ "y": -119.83599853515625
+ },
+ {
+ "x": 579.4119873046875,
+ "y": -114.58000183105469
+ },
+ {
+ "x": 589.7150268554688,
+ "y": -108.7020034790039
+ },
+ {
+ "x": 599.6510009765625,
+ "y": -102.2229995727539
+ },
+ {
+ "x": 609.1859741210938,
+ "y": -95.16699981689453
+ },
+ {
+ "x": 618.2849731445312,
+ "y": -87.55899810791016
+ },
+ {
+ "x": 626.9180297851562,
+ "y": -79.42400360107422
+ },
+ {
+ "x": 635.052978515625,
+ "y": -70.79100036621094
+ },
+ {
+ "x": 642.6619873046875,
+ "y": -61.69200134277344
+ },
+ {
+ "x": 649.718994140625,
+ "y": -52.15700149536133
+ },
+ {
+ "x": 656.197021484375,
+ "y": -42.22100067138672
+ },
+ {
+ "x": 662.0759887695312,
+ "y": -31.91900062561035
+ },
+ {
+ "x": 667.3330078125,
+ "y": -21.285999298095703
+ },
+ {
+ "x": 671.9509887695312,
+ "y": -10.359999656677246
+ },
+ {
+ "x": 675.9119873046875,
+ "y": 0.8199999928474426
+ },
+ {
+ "x": 679.2039794921875,
+ "y": 12.21500015258789
+ },
+ {
+ "x": 681.8150024414062,
+ "y": 23.785999298095703
+ },
+ {
+ "x": 683.7349853515625,
+ "y": 35.492000579833984
+ },
+ {
+ "x": 684.9580078125,
+ "y": 47.290000915527344
+ },
+ {
+ "x": 685.47900390625,
+ "y": 59.13999938964844
+ },
+ {
+ "x": 685.2969970703125,
+ "y": 71
+ },
+ {
+ "x": 684.4119873046875,
+ "y": 82.8290023803711
+ },
+ {
+ "x": 682.8270263671875,
+ "y": 94.58399963378906
+ },
+ {
+ "x": 680.5479736328125,
+ "y": 106.2249984741211
+ },
+ {
+ "x": 677.583984375,
+ "y": 117.70999908447266
+ },
+ {
+ "x": 673.9429931640625,
+ "y": 128.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": 634.8679809570312,
+ "y": 194.99899291992188
+ },
+ {
+ "x": 627.1589965820312,
+ "y": 203.1820068359375
+ },
+ {
+ "x": 619.0020141601562,
+ "y": 210.91900634765625
+ },
+ {
+ "x": 610.4229736328125,
+ "y": 218.18600463867188
+ },
+ {
+ "x": 601.4500122070312,
+ "y": 224.95799255371094
+ },
+ {
+ "x": 592.1099853515625,
+ "y": 231.21600341796875
+ },
+ {
+ "x": 582.4329833984375,
+ "y": 236.93899536132812
+ },
+ {
+ "x": 572.4500122070312,
+ "y": 242.11000061035156
+ },
+ {
+ "x": 562.1920166015625,
+ "y": 246.71099853515625
+ },
+ {
+ "x": 551.6920166015625,
+ "y": 250.72799682617188
+ },
+ {
+ "x": 540.9819946289062,
+ "y": 254.14999389648438
+ },
+ {
+ "x": 530.0980224609375,
+ "y": 256.9639892578125
+ },
+ {
+ "x": 519.072021484375,
+ "y": 259.1619873046875
+ },
+ {
+ "x": 507.94000244140625,
+ "y": 260.73699951171875
+ },
+ {
+ "x": 496.7380065917969,
+ "y": 261.6839904785156
+ },
+ {
+ "x": 485.5,
+ "y": 262
+ },
+ {
+ "x": 474.260986328125,
+ "y": 261.6839904785156
+ },
+ {
+ "x": 463.0589904785156,
+ "y": 260.73699951171875
+ },
+ {
+ "x": 451.927001953125,
+ "y": 259.1619873046875
+ },
+ {
+ "x": 440.9010009765625,
+ "y": 256.9639892578125
+ },
+ {
+ "x": 430.0169982910156,
+ "y": 254.14999389648438
+ },
+ {
+ "x": 419.3070068359375,
+ "y": 250.72799682617188
+ },
+ {
+ "x": 408.8070068359375,
+ "y": 246.71099853515625
+ },
+ {
+ "x": 398.54901123046875,
+ "y": 242.11000061035156
+ },
+ {
+ "x": 388.5660095214844,
+ "y": 236.93899536132812
+ },
+ {
+ "x": 378.8890075683594,
+ "y": 231.21600341796875
+ },
+ {
+ "x": 369.54901123046875,
+ "y": 224.95799255371094
+ },
+ {
+ "x": 360.57598876953125,
+ "y": 218.18600463867188
+ },
+ {
+ "x": 351.99700927734375,
+ "y": 210.91900634765625
+ },
+ {
+ "x": 343.8399963378906,
+ "y": 203.1820068359375
+ },
+ {
+ "x": 336.1310119628906,
+ "y": 195
+ }
+ ],
+ "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": 931.4099731445312,
+ "y": -186.23599243164062
+ },
+ {
+ "x": 950.2620239257812,
+ "y": -182.7899932861328
+ },
+ {
+ "x": 968.697998046875,
+ "y": -177.5540008544922
+ },
+ {
+ "x": 986.5479736328125,
+ "y": -170.57899475097656
+ },
+ {
+ "x": 1003.6480102539062,
+ "y": -161.927001953125
+ },
+ {
+ "x": 1019.8419799804688,
+ "y": -151.677001953125
+ },
+ {
+ "x": 1034.98095703125,
+ "y": -139.9250030517578
+ },
+ {
+ "x": 1048.925048828125,
+ "y": -126.77799987792969
+ },
+ {
+ "x": 1061.5469970703125,
+ "y": -112.35700225830078
+ },
+ {
+ "x": 1072.72998046875,
+ "y": -96.79399871826172
+ },
+ {
+ "x": 1082.373046875,
+ "y": -80.23100280761719
+ },
+ {
+ "x": 1090.385986328125,
+ "y": -62.821998596191406
+ },
+ {
+ "x": 1096.696044921875,
+ "y": -44.72600173950195
+ },
+ {
+ "x": 1101.2449951171875,
+ "y": -26.108999252319336
+ },
+ {
+ "x": 1103.990966796875,
+ "y": -7.142000198364258
+ },
+ {
+ "x": 1104.9100341796875,
+ "y": 12
+ },
+ {
+ "x": 1103.990966796875,
+ "y": 31.142000198364258
+ },
+ {
+ "x": 1101.2449951171875,
+ "y": 50.10900115966797
+ },
+ {
+ "x": 1096.696044921875,
+ "y": 68.72599792480469
+ },
+ {
+ "x": 1090.385986328125,
+ "y": 86.8219985961914
+ },
+ {
+ "x": 1082.373046875,
+ "y": 104.23100280761719
+ },
+ {
+ "x": 1072.72998046875,
+ "y": 120.79399871826172
+ },
+ {
+ "x": 1061.5469970703125,
+ "y": 136.35699462890625
+ },
+ {
+ "x": 1048.925048828125,
+ "y": 150.7779998779297
+ },
+ {
+ "x": 1034.98095703125,
+ "y": 163.9250030517578
+ },
+ {
+ "x": 1019.8419799804688,
+ "y": 175.677001953125
+ },
+ {
+ "x": 1003.6480102539062,
+ "y": 185.927001953125
+ },
+ {
+ "x": 986.5479736328125,
+ "y": 194.57899475097656
+ },
+ {
+ "x": 968.697998046875,
+ "y": 201.5540008544922
+ },
+ {
+ "x": 950.2620239257812,
+ "y": 206.7899932861328
+ },
+ {
+ "x": 931.4099731445312,
+ "y": 210.23599243164062
+ }
+ ],
+ "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..ab9e7b8c70
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
@@ -0,0 +1,95 @@
+abcdabcab
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index 7585d91b3e..70a3985e5b 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -1773,3 +1773,16 @@ style: {fill-pattern: dots; fill:"radial-gradient(#fbfbf8, #e3e3f0)"; stroke: "#
a->b
+-- 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}
+}
diff --git a/lib/geo/segment.go b/lib/geo/segment.go
index ea29df98c9..c902ea890c 100644
--- a/lib/geo/segment.go
+++ b/lib/geo/segment.go
@@ -126,3 +126,41 @@ func (segment Segment) Length() float64 {
func (segment Segment) ToVector() Vector {
return NewVector(segment.End.X-segment.Start.X, segment.End.Y-segment.Start.Y)
}
+
+// IntersectCircle returns intersection points of this segment with a circle
+// centered at the given point with the given radius. Returned points lie on
+// the segment (parameter t in [0,1]). A tangent contact yields a single
+// point; the list is empty when the segment misses the circle, or only
+// touches it outside its endpoints.
+func (s Segment) IntersectCircle(center *Point, radius float64) []*Point {
+ dx := s.End.X - s.Start.X
+ dy := s.End.Y - s.Start.Y
+ fx := s.Start.X - center.X
+ fy := s.Start.Y - center.Y
+ a := dx*dx + dy*dy
+ if a == 0 {
+ return nil
+ }
+ b := 2 * (fx*dx + fy*dy)
+ c := fx*fx + fy*fy - radius*radius
+ disc := b*b - 4*a*c
+ if disc < 0 {
+ return nil
+ }
+ sqrtDisc := math.Sqrt(disc)
+ roots := []float64{(-b - sqrtDisc) / (2 * a)}
+ if disc > 0 {
+ roots = append(roots, (-b+sqrtDisc)/(2*a))
+ }
+ var pts []*Point
+ for _, t := range roots {
+ if t < 0 {
+ continue
+ }
+ if t > 1 {
+ continue
+ }
+ pts = append(pts, NewPoint(s.Start.X+t*dx, s.Start.Y+t*dy))
+ }
+ return pts
+}
diff --git a/lib/geo/segment_test.go b/lib/geo/segment_test.go
index ce2836a7e5..afd8399a5a 100644
--- a/lib/geo/segment_test.go
+++ b/lib/geo/segment_test.go
@@ -31,3 +31,73 @@ func TestSegmentIntersections(t *testing.T) {
intersections = s1.Intersections(*s5)
assert.Equal(t, len(intersections), 0)
}
+
+func TestSegmentIntersectCircle(t *testing.T) {
+ origin := NewPoint(0, 0)
+
+ // segment passing through the origin → 2 intersections at (-r, 0) and (r, 0)
+ s := NewSegment(NewPoint(-10, 0), NewPoint(10, 0))
+ pts := s.IntersectCircle(origin, 5)
+ assert.Equal(t, 2, len(pts))
+ assert.InDelta(t, -5, pts[0].X, 1e-9)
+ assert.InDelta(t, 0, pts[0].Y, 1e-9)
+ assert.InDelta(t, 5, pts[1].X, 1e-9)
+ assert.InDelta(t, 0, pts[1].Y, 1e-9)
+
+ // segment crossing the circle once with one endpoint inside
+ s = NewSegment(NewPoint(0, 0), NewPoint(10, 0))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 1, len(pts))
+ assert.InDelta(t, 5, pts[0].X, 1e-9)
+ assert.InDelta(t, 0, pts[0].Y, 1e-9)
+
+ // segment entirely outside the circle, no crossing
+ s = NewSegment(NewPoint(10, 10), NewPoint(20, 20))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 0, len(pts))
+
+ // segment entirely inside the circle, no crossing
+ s = NewSegment(NewPoint(-1, 0), NewPoint(1, 0))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 0, len(pts))
+
+ // segment endpoint on the circle
+ s = NewSegment(NewPoint(0, 0), NewPoint(5, 0))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 1, len(pts))
+ assert.InDelta(t, 5, pts[0].X, 1e-9)
+ assert.InDelta(t, 0, pts[0].Y, 1e-9)
+
+ // vertical segment chord intersecting the circle at two points
+ s = NewSegment(NewPoint(3, -10), NewPoint(3, 10))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 2, len(pts))
+ assert.InDelta(t, 3, pts[0].X, 1e-9)
+ assert.InDelta(t, -4, pts[0].Y, 1e-9)
+ assert.InDelta(t, 3, pts[1].X, 1e-9)
+ assert.InDelta(t, 4, pts[1].Y, 1e-9)
+
+ // circle centered off origin
+ c := NewPoint(10, 10)
+ s = NewSegment(NewPoint(0, 10), NewPoint(20, 10))
+ pts = s.IntersectCircle(c, 5)
+ assert.Equal(t, 2, len(pts))
+ assert.InDelta(t, 5, pts[0].X, 1e-9)
+ assert.InDelta(t, 10, pts[0].Y, 1e-9)
+ assert.InDelta(t, 15, pts[1].X, 1e-9)
+ assert.InDelta(t, 10, pts[1].Y, 1e-9)
+
+ // tangent contact: the segment grazes the circle at a single point and
+ // must not be reported twice (regression test for the duplicated-root
+ // case when the discriminant is zero).
+ s = NewSegment(NewPoint(-10, 5), NewPoint(10, 5))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Equal(t, 1, len(pts))
+ assert.InDelta(t, 0, pts[0].X, 1e-9)
+ assert.InDelta(t, 5, pts[0].Y, 1e-9)
+
+ // degenerate zero-length segment returns nil
+ s = NewSegment(NewPoint(7, 7), NewPoint(7, 7))
+ pts = s.IntersectCircle(origin, 5)
+ assert.Nil(t, pts)
+}