diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index d291ced933..4405f3ef6b 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -2,6 +2,7 @@
- elk: configure the spacing between edges routed between the same pair of layers with `--elk-edgeEdgeBetweenLayers`. It was previously fixed at 50, which forced large gaps between layers on graphs with many links between adjacent layers.
- renders: add native isometric SVG, PNG, GIF, PDF, and PPTX exports with `--isometric`, source configuration, and Go/JavaScript options
+- shape: `cycle` lays a container's children out on a circle and routes their connections as arcs that start and stop on the shape borders
#### Improvements 🧹
diff --git a/d2graph/cyclediagram.go b/d2graph/cyclediagram.go
new file mode 100644
index 0000000000..65d7903935
--- /dev/null
+++ b/d2graph/cyclediagram.go
@@ -0,0 +1,7 @@
+package d2graph
+
+import "github.com/d2lang/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..5b687d96c0
--- /dev/null
+++ b/d2layouts/d2cycle/layout.go
@@ -0,0 +1,367 @@
+package d2cycle
+
+import (
+ "context"
+ "math"
+
+ "github.com/d2lang/d2/d2graph"
+ "github.com/d2lang/d2/lib/geo"
+ "github.com/d2lang/d2/lib/label"
+ "github.com/d2lang/d2/lib/shape"
+ "github.com/d2lang/util-go/go2"
+)
+
+const (
+ MIN_RADIUS = 200
+ PADDING = 20
+ ARC_STEPS = 30
+ // SELF_LOOP_MIN_RADIUS keeps the loop of a self edge visible even when the
+ // shape it hangs off is tiny.
+ SELF_LOOP_MIN_RADIUS = 20
+)
+
+// 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)
+ }
+
+ normalizeToOrigin(g)
+
+ return nil
+}
+
+// normalizeToOrigin shifts the laid out board so that everything it draws --
+// the shapes and the arcs between them -- lies at or below the origin corner,
+// and records the resulting size on the root. positionObjects centers the ring
+// on (0, 0), which leaves the upper left half of the board in negative
+// coordinates, but d2layouts fits the container to the nested board and then
+// places it at (0, 0), and d2layouts.PositionNested offsets the contents by the
+// container position alone -- both assume the nested board already starts at
+// the origin. Without this the container is drawn offset from the very children
+// it wraps. The arcs are included in the bounds so the container encloses them
+// rather than being clipped to the ring of shapes.
+func normalizeToOrigin(g *d2graph.Graph) {
+ minX, minY := math.Inf(1), math.Inf(1)
+ maxX, maxY := math.Inf(-1), math.Inf(-1)
+ track := func(x, y float64) {
+ minX, minY = math.Min(minX, x), math.Min(minY, y)
+ maxX, maxY = math.Max(maxX, x), math.Max(maxY, y)
+ }
+ for _, obj := range g.Objects {
+ if obj.TopLeft == nil {
+ continue
+ }
+ track(obj.TopLeft.X, obj.TopLeft.Y)
+ track(obj.TopLeft.X+obj.Width, obj.TopLeft.Y+obj.Height)
+ }
+ for _, edge := range g.Edges {
+ for _, p := range edge.Route {
+ track(p.X, p.Y)
+ }
+ }
+ if math.IsInf(minX, 1) || math.IsInf(minY, 1) {
+ return
+ }
+
+ dx, dy := -minX, -minY
+ if dx != 0 || dy != 0 {
+ for _, obj := range g.Root.ChildrenArray {
+ obj.MoveWithDescendants(dx, dy)
+ }
+ for _, edge := range g.Edges {
+ edge.Move(dx, dy)
+ }
+ }
+ // The exporter truncates positions and sizes to whole pixels, and the
+ // circle leaves shapes on half pixels, so a bare width can round down
+ // past a child and draw the container border through it. Round up.
+ g.Root.Width = math.Ceil(maxX - minX)
+ g.Root.Height = math.Ceil(maxY - minY)
+}
+
+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)
+ }
+ if numObjects < 2 {
+ // sin(pi/1) is zero to within rounding, so the expression below
+ // returns about 1e17. Every coordinate on the ring is then spaced
+ // further apart than the shapes themselves, and the arcs computed
+ // from them collapse. One shape needs no room for neighbours.
+ return MIN_RADIUS
+ }
+ 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
+ }
+
+ if edge.Src == edge.Dst {
+ selfLoopRoute(edge)
+ 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
+}
+
+// selfLoopRoute routes an edge whose source and destination are the same shape
+// as a small circle sitting just outside that shape, on the far side from the
+// centre of the layout ring. The circular arc code cannot express a self loop:
+// the two centres coincide, so the sweep is zero and every clipped endpoint
+// collapses onto the same point. That leaves a zero length route, which the
+// renderer turns into NaN coordinates and the render target validator rejects.
+func selfLoopRoute(edge *d2graph.Edge) {
+ box := edge.Src.Box
+ center := edge.Src.Center()
+
+ // The ring is centred on the origin, so the shape centre already points
+ // away from it. A shape sitting exactly on the origin has no outward
+ // direction of its own; send its loop straight up.
+ outX, outY := center.X, center.Y
+ if norm := math.Hypot(outX, outY); norm > 0 {
+ outX, outY = outX/norm, outY/norm
+ } else {
+ outX, outY = 0, -1
+ }
+
+ reach := box.Width + box.Height
+ far := geo.NewPoint(center.X+outX*reach, center.Y+outY*reach)
+ anchor := clipToShapeBorder(edge.Src.ToShape(), box, center, far)
+
+ loopRadius := math.Max(SELF_LOOP_MIN_RADIUS, math.Min(box.Width, box.Height)/3)
+ loopCenter := geo.NewPoint(anchor.X+outX*loopRadius, anchor.Y+outY*loopRadius)
+ // anchor lies on the loop, so start and finish the sweep there.
+ startAngle := math.Atan2(anchor.Y-loopCenter.Y, anchor.X-loopCenter.X)
+
+ path := make([]*geo.Point, 0, ARC_STEPS+1)
+ for i := 0; i <= ARC_STEPS; i++ {
+ angle := startAngle + 2*math.Pi*float64(i)/float64(ARC_STEPS)
+ path = append(path, geo.NewPoint(
+ loopCenter.X+loopRadius*math.Cos(angle),
+ loopCenter.Y+loopRadius*math.Sin(angle),
+ ))
+ }
+
+ 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/d2cycle/layout_test.go b/d2layouts/d2cycle/layout_test.go
new file mode 100644
index 0000000000..6282775bd4
--- /dev/null
+++ b/d2layouts/d2cycle/layout_test.go
@@ -0,0 +1,101 @@
+package d2cycle
+
+import (
+ "math"
+ "testing"
+
+ "github.com/d2lang/d2/d2graph"
+ "github.com/d2lang/d2/lib/geo"
+)
+
+// A curved connection is rendered as a chain of cubic segments, so the render
+// target validator requires 1+3n points on its route. Every route this package
+// emits samples ARC_STEPS+1 points, which ties the constant to that rule: the
+// current value of 30 satisfies it, but nothing in the sampling loops would
+// stop a later edit from picking a value that rejects every arc.
+func TestARCStepsSamplesACubicPointCount(t *testing.T) {
+ points := ARC_STEPS + 1
+ if (points-1)%3 != 0 {
+ t.Fatalf("ARC_STEPS = %d samples %d points, but a curved route needs 1+3n; pick a multiple of 3", ARC_STEPS, points)
+ }
+}
+
+// A cycle holding a single shape has no neighbours to make room for, and the
+// spacing formula is undefined there: sin(pi/1) rounds to 1.2e-16, which sends
+// the radius to about 1e17. Coordinates that large are spaced further apart
+// than the shapes are wide, so anything computed on the ring degenerates.
+func TestCalculateRadiusStaysFiniteForOneObject(t *testing.T) {
+ obj := boxedObject(0, 0, 100, 60)
+
+ radius := calculateRadius([]*d2graph.Object{obj})
+
+ if radius != MIN_RADIUS {
+ t.Fatalf("radius for a single object = %v, want %v", radius, MIN_RADIUS)
+ }
+}
+
+// A self loop shares one centre between source and destination, so the arc
+// geometry degenerates: the sweep is zero and both clipped endpoints land on
+// the same point. Routing it as a straight line therefore yields a zero length
+// route, which renders as NaN and is rejected by the render target validator.
+func TestSelfLoopRouteIsRenderable(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ cx, cy, w, h float64
+ }{
+ {"on the ring", MIN_RADIUS, 0, 100, 60},
+ {"above the ring centre", 0, -MIN_RADIUS, 100, 60},
+ {"off axis", 141, -141, 100, 60},
+ {"tiny shape", MIN_RADIUS, 0, 6, 6},
+ {"at the ring centre", 0, 0, 100, 60},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ obj := boxedObject(tc.cx, tc.cy, tc.w, tc.h)
+ edge := &d2graph.Edge{Src: obj, Dst: obj}
+
+ createCircularArc(edge, MIN_RADIUS)
+
+ if !edge.IsCurve {
+ t.Error("a self loop must be marked as a curve")
+ }
+ if got, want := len(edge.Route), ARC_STEPS+1; got != want {
+ t.Fatalf("route has %d points, want %d", got, want)
+ }
+ for i, point := range edge.Route {
+ if math.IsNaN(point.X) || math.IsNaN(point.Y) || math.IsInf(point.X, 0) || math.IsInf(point.Y, 0) {
+ t.Fatalf("route[%d] = (%v, %v), want a finite point", i, point.X, point.Y)
+ }
+ if i == 0 {
+ continue
+ }
+ if previous := edge.Route[i-1]; previous.X == point.X && previous.Y == point.Y {
+ t.Fatalf("route[%d:%d] is a zero length segment at (%v, %v)", i-1, i, point.X, point.Y)
+ }
+ }
+
+ // A loop squashed onto a line is not a loop. Both sides of its
+ // bounding box have to span the loop, which is at least
+ // SELF_LOOP_MIN_RADIUS across.
+ width, height := routeBounds(edge.Route)
+ if width < SELF_LOOP_MIN_RADIUS || height < SELF_LOOP_MIN_RADIUS {
+ t.Errorf("loop spans %v x %v, want at least %v on both sides", width, height, SELF_LOOP_MIN_RADIUS)
+ }
+ })
+ }
+}
+
+func boxedObject(centerX, centerY, width, height float64) *d2graph.Object {
+ return &d2graph.Object{
+ Box: geo.NewBox(geo.NewPoint(centerX-width/2, centerY-height/2), width, height),
+ }
+}
+
+func routeBounds(route []*geo.Point) (width, height float64) {
+ minX, minY := math.Inf(1), math.Inf(1)
+ maxX, maxY := math.Inf(-1), math.Inf(-1)
+ for _, point := range route {
+ minX, maxX = math.Min(minX, point.X), math.Max(maxX, point.X)
+ minY, maxY = math.Min(minY, point.Y), math.Max(maxY, point.Y)
+ }
+ return maxX - minX, maxY - minY
+}
diff --git a/d2layouts/d2layouts.go b/d2layouts/d2layouts.go
index c9c45479df..30f5ca0f84 100644
--- a/d2layouts/d2layouts.go
+++ b/d2layouts/d2layouts.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/d2lang/d2/d2graph"
+ "github.com/d2lang/d2/d2layouts/d2cycle"
"github.com/d2lang/d2/d2layouts/d2grid"
"github.com/d2lang/d2/d2layouts/d2near"
"github.com/d2lang/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 {
@@ -289,6 +290,12 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
if err != nil {
return err
}
+ case CycleDiagram:
+ log.Debug(ctx, "layout cycle", 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)
@@ -393,6 +400,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/d2raster/target_pixel_coverage_test.go b/d2renderers/d2raster/target_pixel_coverage_test.go
index 5146d4ad3c..efc2b01751 100644
--- a/d2renderers/d2raster/target_pixel_coverage_test.go
+++ b/d2renderers/d2raster/target_pixel_coverage_test.go
@@ -56,6 +56,7 @@ func TestTargetShapePixels(t *testing.T) {
d2target.ShapeSQLTable,
d2target.ShapeImage,
d2target.ShapeSequenceDiagram,
+ d2target.ShapeCycleDiagram,
d2target.ShapeHierarchy,
}
if !reflect.DeepEqual(shapeValues, d2target.Shapes) {
diff --git a/d2renderers/d2raster/testdata/target-shapes.exp.png b/d2renderers/d2raster/testdata/target-shapes.exp.png
index c7126219f9..85f894820e 100644
Binary files a/d2renderers/d2raster/testdata/target-shapes.exp.png and b/d2renderers/d2raster/testdata/target-shapes.exp.png differ
diff --git a/d2renderers/d2scenebuild/shape_effects.go b/d2renderers/d2scenebuild/shape_effects.go
index 8d241ec1a1..ec30c3a7d8 100644
--- a/d2renderers/d2scenebuild/shape_effects.go
+++ b/d2renderers/d2scenebuild/shape_effects.go
@@ -295,7 +295,7 @@ func (b *builder) buildOrdinaryShapeOutline(targetShape d2target.Shape, fill d2s
Fill: fill,
Stroke: stroke,
}))
- case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
+ case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "":
radius := clampBorderRadius(float64(targetShape.BorderRadius), box.Width, box.Height)
nodes = append(nodes, d2scene.NewNode(d2scene.Rect{
Box: box,
diff --git a/d2renderers/d2scenebuild/sketch_shapes.go b/d2renderers/d2scenebuild/sketch_shapes.go
index 9f1b1f15fe..56accc2ff1 100644
--- a/d2renderers/d2scenebuild/sketch_shapes.go
+++ b/d2renderers/d2scenebuild/sketch_shapes.go
@@ -127,7 +127,7 @@ func (b *builder) buildSketchOrdinaryShape(targetShape d2target.Shape, idPrefix
object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset,
drawable, d2scene.Translate(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), targetShape,
)
- case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
+ case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "":
drawable := newSketchGenerator().Rectangle(0, 0, float64(targetShape.Width), float64(targetShape.Height), options)
nodes, err = b.compileSketchShapeDrawable(
object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset,
@@ -156,7 +156,7 @@ func (b *builder) buildSketchOrdinaryShape(targetShape d2target.Shape, idPrefix
RadiusX: float64(targetShape.Width) / 2,
RadiusY: float64(targetShape.Height) / 2,
})
- case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
+ case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "":
overlayPrimitives = append(overlayPrimitives, d2scene.Rect{Box: sketchShapeBox(targetShape)})
default:
for _, node := range roughNodes {
@@ -578,7 +578,7 @@ func (b *builder) buildSketchOutlineOnly(targetShape d2target.Shape, idPrefix st
),
d2scene.Translate(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), targetShape,
)
- case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
+ case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "":
return b.compileSketchShapeDrawable(
object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset,
newSketchGenerator().Rectangle(0, 0, float64(targetShape.Width), float64(targetShape.Height), options),
diff --git a/d2renderers/d2scenebuild/sketch_shapes_test.go b/d2renderers/d2scenebuild/sketch_shapes_test.go
index 18040f1263..8792075369 100644
--- a/d2renderers/d2scenebuild/sketch_shapes_test.go
+++ b/d2renderers/d2scenebuild/sketch_shapes_test.go
@@ -28,7 +28,7 @@ func TestSketchOrdinaryShapeFamiliesUseTypedRoughPathsDeterministically(t *testi
d2target.ShapeStep, d2target.ShapeCallout, d2target.ShapeStoredData,
d2target.ShapePerson, d2target.ShapeC4Person, d2target.ShapeDiamond,
d2target.ShapeHexagon, d2target.ShapeCloud,
- d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy,
+ d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy,
}
for _, shapeType := range shapeTypes {
name := shapeType
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 412e9abb2f..15f3fe1c5b 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -1831,7 +1831,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
fmt.Fprint(writer, el.Render())
// TODO should standardize "" to rectangle
- case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
+ case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "":
borderRadius := math.MaxFloat64
if targetShape.BorderRadius != 0 {
borderRadius = float64(targetShape.BorderRadius)
diff --git a/d2target/d2target.go b/d2target/d2target.go
index ef9ee67bd3..b8a70edd04 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -1267,6 +1267,7 @@ const (
ShapeSQLTable = "sql_table"
ShapeImage = "image"
ShapeSequenceDiagram = "sequence_diagram"
+ ShapeCycleDiagram = "cycle"
ShapeHierarchy = "hierarchy"
)
@@ -1295,6 +1296,7 @@ var Shapes = []string{
ShapeSQLTable,
ShapeImage,
ShapeSequenceDiagram,
+ ShapeCycleDiagram,
ShapeHierarchy,
}
@@ -1365,6 +1367,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..ba65555f70
--- /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": 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": 200,
+ "y": 0
+ },
+ "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": 400,
+ "y": 200
+ },
+ "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": 200,
+ "y": 400
+ },
+ "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": 0,
+ "y": 200
+ },
+ "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": 514,
+ "y": 17
+ },
+ "width": 400,
+ "height": 433,
+ "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": 687,
+ "y": 17
+ },
+ "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": 860,
+ "y": 317
+ },
+ "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": 514,
+ "y": 317
+ },
+ "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": 974,
+ "y": 0
+ },
+ "width": 227,
+ "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": 974,
+ "y": 0
+ },
+ "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": 974,
+ "y": 400
+ },
+ "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": 253.5,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 261.8789978027344,
+ "y": 36.06399917602539
+ },
+ {
+ "x": 270.1969909667969,
+ "y": 37.720001220703125
+ },
+ {
+ "x": 278.4360046386719,
+ "y": 39.72700119018555
+ },
+ {
+ "x": 286.5830078125,
+ "y": 42.08100128173828
+ },
+ {
+ "x": 294.62298583984375,
+ "y": 44.77899932861328
+ },
+ {
+ "x": 302.5409851074219,
+ "y": 47.814998626708984
+ },
+ {
+ "x": 310.3240051269531,
+ "y": 51.18299865722656
+ },
+ {
+ "x": 317.95599365234375,
+ "y": 54.87900161743164
+ },
+ {
+ "x": 325.4259948730469,
+ "y": 58.89500045776367
+ },
+ {
+ "x": 332.7179870605469,
+ "y": 63.2239990234375
+ },
+ {
+ "x": 339.82000732421875,
+ "y": 67.85800170898438
+ },
+ {
+ "x": 346.718994140625,
+ "y": 72.79000091552734
+ },
+ {
+ "x": 353.40301513671875,
+ "y": 78.00900268554688
+ },
+ {
+ "x": 359.8599853515625,
+ "y": 83.50700378417969
+ },
+ {
+ "x": 366.0780029296875,
+ "y": 89.27300262451172
+ },
+ {
+ "x": 372.0459899902344,
+ "y": 95.2979965209961
+ },
+ {
+ "x": 377.75299072265625,
+ "y": 101.57099914550781
+ },
+ {
+ "x": 383.1889953613281,
+ "y": 108.08000183105469
+ },
+ {
+ "x": 388.343994140625,
+ "y": 114.81300354003906
+ },
+ {
+ "x": 393.2090148925781,
+ "y": 121.75900268554688
+ },
+ {
+ "x": 397.7749938964844,
+ "y": 128.90499877929688
+ },
+ {
+ "x": 402.0350036621094,
+ "y": 136.23800659179688
+ },
+ {
+ "x": 405.97900390625,
+ "y": 143.74600219726562
+ },
+ {
+ "x": 409.60198974609375,
+ "y": 151.41299438476562
+ },
+ {
+ "x": 412.89599609375,
+ "y": 159.2270050048828
+ },
+ {
+ "x": 415.85699462890625,
+ "y": 167.1739959716797
+ },
+ {
+ "x": 418.47698974609375,
+ "y": 175.24000549316406
+ },
+ {
+ "x": 420.7539978027344,
+ "y": 183.40899658203125
+ },
+ {
+ "x": 422.6820068359375,
+ "y": 191.66700744628906
+ },
+ {
+ "x": 424.25799560546875,
+ "y": 200
+ }
+ ],
+ "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": 424.25799560546875,
+ "y": 266
+ },
+ {
+ "x": 422.6820068359375,
+ "y": 274.3320007324219
+ },
+ {
+ "x": 420.7539978027344,
+ "y": 282.5899963378906
+ },
+ {
+ "x": 418.47698974609375,
+ "y": 290.7590026855469
+ },
+ {
+ "x": 415.85699462890625,
+ "y": 298.82501220703125
+ },
+ {
+ "x": 412.89599609375,
+ "y": 306.7720031738281
+ },
+ {
+ "x": 409.60198974609375,
+ "y": 314.58599853515625
+ },
+ {
+ "x": 405.97900390625,
+ "y": 322.25299072265625
+ },
+ {
+ "x": 402.0350036621094,
+ "y": 329.760986328125
+ },
+ {
+ "x": 397.7749938964844,
+ "y": 337.093994140625
+ },
+ {
+ "x": 393.2090148925781,
+ "y": 344.239990234375
+ },
+ {
+ "x": 388.343994140625,
+ "y": 351.1860046386719
+ },
+ {
+ "x": 383.1889953613281,
+ "y": 357.91900634765625
+ },
+ {
+ "x": 377.75299072265625,
+ "y": 364.4280090332031
+ },
+ {
+ "x": 372.0459899902344,
+ "y": 370.70098876953125
+ },
+ {
+ "x": 366.0780029296875,
+ "y": 376.72601318359375
+ },
+ {
+ "x": 359.8599853515625,
+ "y": 382.49200439453125
+ },
+ {
+ "x": 353.40301513671875,
+ "y": 387.989990234375
+ },
+ {
+ "x": 346.718994140625,
+ "y": 393.2090148925781
+ },
+ {
+ "x": 339.82000732421875,
+ "y": 398.1409912109375
+ },
+ {
+ "x": 332.7179870605469,
+ "y": 402.7749938964844
+ },
+ {
+ "x": 325.4259948730469,
+ "y": 407.10400390625
+ },
+ {
+ "x": 317.95599365234375,
+ "y": 411.1199951171875
+ },
+ {
+ "x": 310.3240051269531,
+ "y": 414.8160095214844
+ },
+ {
+ "x": 302.5409851074219,
+ "y": 418.1839904785156
+ },
+ {
+ "x": 294.62298583984375,
+ "y": 421.2200012207031
+ },
+ {
+ "x": 286.5830078125,
+ "y": 423.9179992675781
+ },
+ {
+ "x": 278.4360046386719,
+ "y": 426.2720031738281
+ },
+ {
+ "x": 270.1969909667969,
+ "y": 428.27899169921875
+ },
+ {
+ "x": 261.8789978027344,
+ "y": 429.93499755859375
+ },
+ {
+ "x": 253.5,
+ "y": 431.2359924316406
+ }
+ ],
+ "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": 200.5,
+ "y": 431.2359924316406
+ },
+ {
+ "x": 192.1199951171875,
+ "y": 429.93499755859375
+ },
+ {
+ "x": 183.802001953125,
+ "y": 428.27899169921875
+ },
+ {
+ "x": 175.56300354003906,
+ "y": 426.2720031738281
+ },
+ {
+ "x": 167.41600036621094,
+ "y": 423.9179992675781
+ },
+ {
+ "x": 159.37600708007812,
+ "y": 421.2200012207031
+ },
+ {
+ "x": 151.45799255371094,
+ "y": 418.1839904785156
+ },
+ {
+ "x": 143.6750030517578,
+ "y": 414.8160095214844
+ },
+ {
+ "x": 136.04299926757812,
+ "y": 411.1199951171875
+ },
+ {
+ "x": 128.572998046875,
+ "y": 407.10400390625
+ },
+ {
+ "x": 121.28099822998047,
+ "y": 402.7749938964844
+ },
+ {
+ "x": 114.17900085449219,
+ "y": 398.1409912109375
+ },
+ {
+ "x": 107.27999877929688,
+ "y": 393.2090148925781
+ },
+ {
+ "x": 100.59600067138672,
+ "y": 387.989990234375
+ },
+ {
+ "x": 94.13899993896484,
+ "y": 382.49200439453125
+ },
+ {
+ "x": 87.9209976196289,
+ "y": 376.72601318359375
+ },
+ {
+ "x": 81.9530029296875,
+ "y": 370.70098876953125
+ },
+ {
+ "x": 76.24600219726562,
+ "y": 364.4280090332031
+ },
+ {
+ "x": 70.80999755859375,
+ "y": 357.91900634765625
+ },
+ {
+ "x": 65.65499877929688,
+ "y": 351.1860046386719
+ },
+ {
+ "x": 60.790000915527344,
+ "y": 344.239990234375
+ },
+ {
+ "x": 56.2239990234375,
+ "y": 337.093994140625
+ },
+ {
+ "x": 51.9640007019043,
+ "y": 329.760986328125
+ },
+ {
+ "x": 48.02000045776367,
+ "y": 322.25299072265625
+ },
+ {
+ "x": 44.39699935913086,
+ "y": 314.58599853515625
+ },
+ {
+ "x": 41.10300064086914,
+ "y": 306.7720031738281
+ },
+ {
+ "x": 38.141998291015625,
+ "y": 298.82501220703125
+ },
+ {
+ "x": 35.52199935913086,
+ "y": 290.7590026855469
+ },
+ {
+ "x": 33.244998931884766,
+ "y": 282.5899963378906
+ },
+ {
+ "x": 31.316999435424805,
+ "y": 274.3320007324219
+ },
+ {
+ "x": 29.740999221801758,
+ "y": 266
+ }
+ ],
+ "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": 740.2050170898438,
+ "y": 51.76300048828125
+ },
+ {
+ "x": 751.9099731445312,
+ "y": 53.68299865722656
+ },
+ {
+ "x": 763.4810180664062,
+ "y": 56.292999267578125
+ },
+ {
+ "x": 774.8770141601562,
+ "y": 59.58399963378906
+ },
+ {
+ "x": 786.0570068359375,
+ "y": 63.54600143432617
+ },
+ {
+ "x": 796.9829711914062,
+ "y": 68.16300201416016
+ },
+ {
+ "x": 807.6170043945312,
+ "y": 73.41899871826172
+ },
+ {
+ "x": 817.9199829101562,
+ "y": 79.2969970703125
+ },
+ {
+ "x": 827.8560180664062,
+ "y": 85.7760009765625
+ },
+ {
+ "x": 837.3909912109375,
+ "y": 92.83200073242188
+ },
+ {
+ "x": 846.489990234375,
+ "y": 100.44000244140625
+ },
+ {
+ "x": 855.1229858398438,
+ "y": 108.57499694824219
+ },
+ {
+ "x": 863.2579956054688,
+ "y": 117.20800018310547
+ },
+ {
+ "x": 870.8670043945312,
+ "y": 126.30699920654297
+ },
+ {
+ "x": 877.9240112304688,
+ "y": 135.8419952392578
+ },
+ {
+ "x": 884.4030151367188,
+ "y": 145.7779998779297
+ },
+ {
+ "x": 890.281005859375,
+ "y": 156.0800018310547
+ },
+ {
+ "x": 895.5380249023438,
+ "y": 166.71299743652344
+ },
+ {
+ "x": 900.156005859375,
+ "y": 177.63900756835938
+ },
+ {
+ "x": 904.1170043945312,
+ "y": 188.82000732421875
+ },
+ {
+ "x": 907.4089965820312,
+ "y": 200.21499633789062
+ },
+ {
+ "x": 910.02001953125,
+ "y": 211.78599548339844
+ },
+ {
+ "x": 911.9400024414062,
+ "y": 223.49200439453125
+ },
+ {
+ "x": 913.1630249023438,
+ "y": 235.2899932861328
+ },
+ {
+ "x": 913.6840209960938,
+ "y": 247.13999938964844
+ },
+ {
+ "x": 913.5020141601562,
+ "y": 259
+ },
+ {
+ "x": 912.6170043945312,
+ "y": 270.8290100097656
+ },
+ {
+ "x": 911.031982421875,
+ "y": 282.5840148925781
+ },
+ {
+ "x": 908.7540283203125,
+ "y": 294.2250061035156
+ },
+ {
+ "x": 905.7890014648438,
+ "y": 305.7099914550781
+ },
+ {
+ "x": 902.1480102539062,
+ "y": 317
+ }
+ ],
+ "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": 863.072998046875,
+ "y": 383
+ },
+ {
+ "x": 855.364013671875,
+ "y": 391.1820068359375
+ },
+ {
+ "x": 847.2069702148438,
+ "y": 398.91900634765625
+ },
+ {
+ "x": 838.6279907226562,
+ "y": 406.1860046386719
+ },
+ {
+ "x": 829.655029296875,
+ "y": 412.9580078125
+ },
+ {
+ "x": 820.3150024414062,
+ "y": 419.21600341796875
+ },
+ {
+ "x": 810.6380004882812,
+ "y": 424.9389953613281
+ },
+ {
+ "x": 800.655029296875,
+ "y": 430.1099853515625
+ },
+ {
+ "x": 790.39697265625,
+ "y": 434.71099853515625
+ },
+ {
+ "x": 779.89697265625,
+ "y": 438.7279968261719
+ },
+ {
+ "x": 769.18701171875,
+ "y": 442.1499938964844
+ },
+ {
+ "x": 758.302978515625,
+ "y": 444.9639892578125
+ },
+ {
+ "x": 747.2769775390625,
+ "y": 447.1619873046875
+ },
+ {
+ "x": 736.14501953125,
+ "y": 448.73699951171875
+ },
+ {
+ "x": 724.9429931640625,
+ "y": 449.6839904785156
+ },
+ {
+ "x": 713.7050170898438,
+ "y": 450
+ },
+ {
+ "x": 702.4660034179688,
+ "y": 449.6839904785156
+ },
+ {
+ "x": 691.2639770507812,
+ "y": 448.73699951171875
+ },
+ {
+ "x": 680.1320190429688,
+ "y": 447.1619873046875
+ },
+ {
+ "x": 669.1069946289062,
+ "y": 444.9639892578125
+ },
+ {
+ "x": 658.2219848632812,
+ "y": 442.1499938964844
+ },
+ {
+ "x": 647.5130004882812,
+ "y": 438.7279968261719
+ },
+ {
+ "x": 637.0120239257812,
+ "y": 434.71099853515625
+ },
+ {
+ "x": 626.7540283203125,
+ "y": 430.1099853515625
+ },
+ {
+ "x": 616.77099609375,
+ "y": 424.9389953613281
+ },
+ {
+ "x": 607.094970703125,
+ "y": 419.21600341796875
+ },
+ {
+ "x": 597.7550048828125,
+ "y": 412.9580078125
+ },
+ {
+ "x": 588.781005859375,
+ "y": 406.1860046386719
+ },
+ {
+ "x": 580.2020263671875,
+ "y": 398.91900634765625
+ },
+ {
+ "x": 572.0449829101562,
+ "y": 391.1820068359375
+ },
+ {
+ "x": 564.3359985351562,
+ "y": 383
+ }
+ ],
+ "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": 1027,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 1045.85205078125,
+ "y": 38.20899963378906
+ },
+ {
+ "x": 1064.2879638671875,
+ "y": 43.44499969482422
+ },
+ {
+ "x": 1082.137939453125,
+ "y": 50.41999816894531
+ },
+ {
+ "x": 1099.238037109375,
+ "y": 59.071998596191406
+ },
+ {
+ "x": 1115.4320068359375,
+ "y": 69.3219985961914
+ },
+ {
+ "x": 1130.571044921875,
+ "y": 81.0739974975586
+ },
+ {
+ "x": 1144.5150146484375,
+ "y": 94.22100067138672
+ },
+ {
+ "x": 1157.136962890625,
+ "y": 108.64199829101562
+ },
+ {
+ "x": 1168.3199462890625,
+ "y": 124.20500183105469
+ },
+ {
+ "x": 1177.9630126953125,
+ "y": 140.76800537109375
+ },
+ {
+ "x": 1185.9759521484375,
+ "y": 158.177001953125
+ },
+ {
+ "x": 1192.2860107421875,
+ "y": 176.2729949951172
+ },
+ {
+ "x": 1196.8349609375,
+ "y": 194.88999938964844
+ },
+ {
+ "x": 1199.5810546875,
+ "y": 213.85699462890625
+ },
+ {
+ "x": 1200.5,
+ "y": 233
+ },
+ {
+ "x": 1199.5810546875,
+ "y": 252.14199829101562
+ },
+ {
+ "x": 1196.8349609375,
+ "y": 271.1090087890625
+ },
+ {
+ "x": 1192.2860107421875,
+ "y": 289.72601318359375
+ },
+ {
+ "x": 1185.9759521484375,
+ "y": 307.8219909667969
+ },
+ {
+ "x": 1177.9630126953125,
+ "y": 325.2309875488281
+ },
+ {
+ "x": 1168.3199462890625,
+ "y": 341.79400634765625
+ },
+ {
+ "x": 1157.136962890625,
+ "y": 357.35699462890625
+ },
+ {
+ "x": 1144.5150146484375,
+ "y": 371.77801513671875
+ },
+ {
+ "x": 1130.571044921875,
+ "y": 384.92498779296875
+ },
+ {
+ "x": 1115.4320068359375,
+ "y": 396.677001953125
+ },
+ {
+ "x": 1099.238037109375,
+ "y": 406.927001953125
+ },
+ {
+ "x": 1082.137939453125,
+ "y": 415.5790100097656
+ },
+ {
+ "x": 1064.2879638671875,
+ "y": 422.5539855957031
+ },
+ {
+ "x": 1045.85205078125,
+ "y": 427.7900085449219
+ },
+ {
+ "x": 1027,
+ "y": 431.2359924316406
+ }
+ ],
+ "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/isometric.exp.svg b/e2etests/testdata/txtar/cycle-diagram/dagre/isometric.exp.svg
new file mode 100644
index 0000000000..1ac53d5781
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/isometric.exp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
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..40257d2d40
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
@@ -0,0 +1,29 @@
+
\ 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..720b49c4ec
--- /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": 212,
+ "y": 12
+ },
+ "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": 412,
+ "y": 212
+ },
+ "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": 212,
+ "y": 412
+ },
+ "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": 12,
+ "y": 212
+ },
+ "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": 486,
+ "y": 28
+ },
+ "width": 400,
+ "height": 433,
+ "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": 659,
+ "y": 28
+ },
+ "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": 832,
+ "y": 328
+ },
+ "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": 486,
+ "y": 328
+ },
+ "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": 906,
+ "y": 12
+ },
+ "width": 227,
+ "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": 906,
+ "y": 12
+ },
+ "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": 906,
+ "y": 412
+ },
+ "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": 265.5,
+ "y": 46.76300048828125
+ },
+ {
+ "x": 273.8789978027344,
+ "y": 48.06399917602539
+ },
+ {
+ "x": 282.1969909667969,
+ "y": 49.720001220703125
+ },
+ {
+ "x": 290.4360046386719,
+ "y": 51.72700119018555
+ },
+ {
+ "x": 298.5830078125,
+ "y": 54.08100128173828
+ },
+ {
+ "x": 306.62298583984375,
+ "y": 56.77899932861328
+ },
+ {
+ "x": 314.5409851074219,
+ "y": 59.814998626708984
+ },
+ {
+ "x": 322.3240051269531,
+ "y": 63.18299865722656
+ },
+ {
+ "x": 329.95599365234375,
+ "y": 66.87899780273438
+ },
+ {
+ "x": 337.4259948730469,
+ "y": 70.8949966430664
+ },
+ {
+ "x": 344.7179870605469,
+ "y": 75.2239990234375
+ },
+ {
+ "x": 351.82000732421875,
+ "y": 79.85800170898438
+ },
+ {
+ "x": 358.718994140625,
+ "y": 84.79000091552734
+ },
+ {
+ "x": 365.40301513671875,
+ "y": 90.00900268554688
+ },
+ {
+ "x": 371.8599853515625,
+ "y": 95.50700378417969
+ },
+ {
+ "x": 378.0780029296875,
+ "y": 101.27300262451172
+ },
+ {
+ "x": 384.0459899902344,
+ "y": 107.2979965209961
+ },
+ {
+ "x": 389.75299072265625,
+ "y": 113.57099914550781
+ },
+ {
+ "x": 395.1889953613281,
+ "y": 120.08000183105469
+ },
+ {
+ "x": 400.343994140625,
+ "y": 126.81300354003906
+ },
+ {
+ "x": 405.2090148925781,
+ "y": 133.75900268554688
+ },
+ {
+ "x": 409.7749938964844,
+ "y": 140.90499877929688
+ },
+ {
+ "x": 414.0350036621094,
+ "y": 148.23800659179688
+ },
+ {
+ "x": 417.97900390625,
+ "y": 155.74600219726562
+ },
+ {
+ "x": 421.60198974609375,
+ "y": 163.41299438476562
+ },
+ {
+ "x": 424.89599609375,
+ "y": 171.2270050048828
+ },
+ {
+ "x": 427.85699462890625,
+ "y": 179.1739959716797
+ },
+ {
+ "x": 430.47698974609375,
+ "y": 187.24000549316406
+ },
+ {
+ "x": 432.7539978027344,
+ "y": 195.40899658203125
+ },
+ {
+ "x": 434.6820068359375,
+ "y": 203.66700744628906
+ },
+ {
+ "x": 436.25799560546875,
+ "y": 212
+ }
+ ],
+ "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": 436.25799560546875,
+ "y": 278
+ },
+ {
+ "x": 434.6820068359375,
+ "y": 286.3320007324219
+ },
+ {
+ "x": 432.7539978027344,
+ "y": 294.5899963378906
+ },
+ {
+ "x": 430.47698974609375,
+ "y": 302.7590026855469
+ },
+ {
+ "x": 427.85699462890625,
+ "y": 310.82501220703125
+ },
+ {
+ "x": 424.89599609375,
+ "y": 318.7720031738281
+ },
+ {
+ "x": 421.60198974609375,
+ "y": 326.58599853515625
+ },
+ {
+ "x": 417.97900390625,
+ "y": 334.25299072265625
+ },
+ {
+ "x": 414.0350036621094,
+ "y": 341.760986328125
+ },
+ {
+ "x": 409.7749938964844,
+ "y": 349.093994140625
+ },
+ {
+ "x": 405.2090148925781,
+ "y": 356.239990234375
+ },
+ {
+ "x": 400.343994140625,
+ "y": 363.1860046386719
+ },
+ {
+ "x": 395.1889953613281,
+ "y": 369.91900634765625
+ },
+ {
+ "x": 389.75299072265625,
+ "y": 376.4280090332031
+ },
+ {
+ "x": 384.0459899902344,
+ "y": 382.70098876953125
+ },
+ {
+ "x": 378.0780029296875,
+ "y": 388.72601318359375
+ },
+ {
+ "x": 371.8599853515625,
+ "y": 394.49200439453125
+ },
+ {
+ "x": 365.40301513671875,
+ "y": 399.989990234375
+ },
+ {
+ "x": 358.718994140625,
+ "y": 405.2090148925781
+ },
+ {
+ "x": 351.82000732421875,
+ "y": 410.1409912109375
+ },
+ {
+ "x": 344.7179870605469,
+ "y": 414.7749938964844
+ },
+ {
+ "x": 337.4259948730469,
+ "y": 419.10400390625
+ },
+ {
+ "x": 329.95599365234375,
+ "y": 423.1199951171875
+ },
+ {
+ "x": 322.3240051269531,
+ "y": 426.8160095214844
+ },
+ {
+ "x": 314.5409851074219,
+ "y": 430.1839904785156
+ },
+ {
+ "x": 306.62298583984375,
+ "y": 433.2200012207031
+ },
+ {
+ "x": 298.5830078125,
+ "y": 435.9179992675781
+ },
+ {
+ "x": 290.4360046386719,
+ "y": 438.2720031738281
+ },
+ {
+ "x": 282.1969909667969,
+ "y": 440.27899169921875
+ },
+ {
+ "x": 273.8789978027344,
+ "y": 441.93499755859375
+ },
+ {
+ "x": 265.5,
+ "y": 443.2359924316406
+ }
+ ],
+ "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": 212.5,
+ "y": 443.2359924316406
+ },
+ {
+ "x": 204.1199951171875,
+ "y": 441.93499755859375
+ },
+ {
+ "x": 195.802001953125,
+ "y": 440.27899169921875
+ },
+ {
+ "x": 187.56300354003906,
+ "y": 438.2720031738281
+ },
+ {
+ "x": 179.41600036621094,
+ "y": 435.9179992675781
+ },
+ {
+ "x": 171.37600708007812,
+ "y": 433.2200012207031
+ },
+ {
+ "x": 163.45799255371094,
+ "y": 430.1839904785156
+ },
+ {
+ "x": 155.6750030517578,
+ "y": 426.8160095214844
+ },
+ {
+ "x": 148.04299926757812,
+ "y": 423.1199951171875
+ },
+ {
+ "x": 140.572998046875,
+ "y": 419.10400390625
+ },
+ {
+ "x": 133.281005859375,
+ "y": 414.7749938964844
+ },
+ {
+ "x": 126.17900085449219,
+ "y": 410.1409912109375
+ },
+ {
+ "x": 119.27999877929688,
+ "y": 405.2090148925781
+ },
+ {
+ "x": 112.59600067138672,
+ "y": 399.989990234375
+ },
+ {
+ "x": 106.13899993896484,
+ "y": 394.49200439453125
+ },
+ {
+ "x": 99.9209976196289,
+ "y": 388.72601318359375
+ },
+ {
+ "x": 93.9530029296875,
+ "y": 382.70098876953125
+ },
+ {
+ "x": 88.24600219726562,
+ "y": 376.4280090332031
+ },
+ {
+ "x": 82.80999755859375,
+ "y": 369.91900634765625
+ },
+ {
+ "x": 77.65499877929688,
+ "y": 363.1860046386719
+ },
+ {
+ "x": 72.79000091552734,
+ "y": 356.239990234375
+ },
+ {
+ "x": 68.2239990234375,
+ "y": 349.093994140625
+ },
+ {
+ "x": 63.9640007019043,
+ "y": 341.760986328125
+ },
+ {
+ "x": 60.02000045776367,
+ "y": 334.25299072265625
+ },
+ {
+ "x": 56.39699935913086,
+ "y": 326.58599853515625
+ },
+ {
+ "x": 53.10300064086914,
+ "y": 318.7720031738281
+ },
+ {
+ "x": 50.141998291015625,
+ "y": 310.82501220703125
+ },
+ {
+ "x": 47.52199935913086,
+ "y": 302.7590026855469
+ },
+ {
+ "x": 45.244998931884766,
+ "y": 294.5899963378906
+ },
+ {
+ "x": 43.31700134277344,
+ "y": 286.3320007324219
+ },
+ {
+ "x": 41.74100112915039,
+ "y": 278
+ }
+ ],
+ "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": 712.2050170898438,
+ "y": 63.26300048828125
+ },
+ {
+ "x": 723.9099731445312,
+ "y": 65.18299865722656
+ },
+ {
+ "x": 735.4810180664062,
+ "y": 67.79299926757812
+ },
+ {
+ "x": 746.8770141601562,
+ "y": 71.08399963378906
+ },
+ {
+ "x": 758.0570068359375,
+ "y": 75.0459976196289
+ },
+ {
+ "x": 768.9829711914062,
+ "y": 79.66300201416016
+ },
+ {
+ "x": 779.6170043945312,
+ "y": 84.91899871826172
+ },
+ {
+ "x": 789.9199829101562,
+ "y": 90.7969970703125
+ },
+ {
+ "x": 799.8560180664062,
+ "y": 97.2760009765625
+ },
+ {
+ "x": 809.3909912109375,
+ "y": 104.33200073242188
+ },
+ {
+ "x": 818.489990234375,
+ "y": 111.94000244140625
+ },
+ {
+ "x": 827.1229858398438,
+ "y": 120.07499694824219
+ },
+ {
+ "x": 835.2579956054688,
+ "y": 128.70799255371094
+ },
+ {
+ "x": 842.8670043945312,
+ "y": 137.8070068359375
+ },
+ {
+ "x": 849.9240112304688,
+ "y": 147.3419952392578
+ },
+ {
+ "x": 856.4030151367188,
+ "y": 157.2779998779297
+ },
+ {
+ "x": 862.281005859375,
+ "y": 167.5800018310547
+ },
+ {
+ "x": 867.5380249023438,
+ "y": 178.21299743652344
+ },
+ {
+ "x": 872.156005859375,
+ "y": 189.13900756835938
+ },
+ {
+ "x": 876.1170043945312,
+ "y": 200.32000732421875
+ },
+ {
+ "x": 879.4089965820312,
+ "y": 211.71499633789062
+ },
+ {
+ "x": 882.02001953125,
+ "y": 223.28599548339844
+ },
+ {
+ "x": 883.9400024414062,
+ "y": 234.99200439453125
+ },
+ {
+ "x": 885.1630249023438,
+ "y": 246.7899932861328
+ },
+ {
+ "x": 885.6840209960938,
+ "y": 258.6400146484375
+ },
+ {
+ "x": 885.5020141601562,
+ "y": 270.5
+ },
+ {
+ "x": 884.6170043945312,
+ "y": 282.3290100097656
+ },
+ {
+ "x": 883.031982421875,
+ "y": 294.0840148925781
+ },
+ {
+ "x": 880.7540283203125,
+ "y": 305.7250061035156
+ },
+ {
+ "x": 877.7890014648438,
+ "y": 317.2099914550781
+ },
+ {
+ "x": 874.1480102539062,
+ "y": 328.5
+ }
+ ],
+ "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": 835.072998046875,
+ "y": 394.5
+ },
+ {
+ "x": 827.364013671875,
+ "y": 402.6820068359375
+ },
+ {
+ "x": 819.2069702148438,
+ "y": 410.41900634765625
+ },
+ {
+ "x": 810.6279907226562,
+ "y": 417.6860046386719
+ },
+ {
+ "x": 801.655029296875,
+ "y": 424.4580078125
+ },
+ {
+ "x": 792.3150024414062,
+ "y": 430.71600341796875
+ },
+ {
+ "x": 782.6380004882812,
+ "y": 436.4389953613281
+ },
+ {
+ "x": 772.655029296875,
+ "y": 441.6099853515625
+ },
+ {
+ "x": 762.39697265625,
+ "y": 446.21099853515625
+ },
+ {
+ "x": 751.89697265625,
+ "y": 450.2279968261719
+ },
+ {
+ "x": 741.18701171875,
+ "y": 453.6499938964844
+ },
+ {
+ "x": 730.302978515625,
+ "y": 456.4639892578125
+ },
+ {
+ "x": 719.2769775390625,
+ "y": 458.6619873046875
+ },
+ {
+ "x": 708.14501953125,
+ "y": 460.23699951171875
+ },
+ {
+ "x": 696.9429931640625,
+ "y": 461.1839904785156
+ },
+ {
+ "x": 685.7050170898438,
+ "y": 461.5
+ },
+ {
+ "x": 674.4660034179688,
+ "y": 461.1839904785156
+ },
+ {
+ "x": 663.2639770507812,
+ "y": 460.23699951171875
+ },
+ {
+ "x": 652.1320190429688,
+ "y": 458.6619873046875
+ },
+ {
+ "x": 641.1069946289062,
+ "y": 456.4639892578125
+ },
+ {
+ "x": 630.2219848632812,
+ "y": 453.6499938964844
+ },
+ {
+ "x": 619.5130004882812,
+ "y": 450.2279968261719
+ },
+ {
+ "x": 609.0120239257812,
+ "y": 446.21099853515625
+ },
+ {
+ "x": 598.7540283203125,
+ "y": 441.6099853515625
+ },
+ {
+ "x": 588.77099609375,
+ "y": 436.4389953613281
+ },
+ {
+ "x": 579.094970703125,
+ "y": 430.71600341796875
+ },
+ {
+ "x": 569.7550048828125,
+ "y": 424.4580078125
+ },
+ {
+ "x": 560.781005859375,
+ "y": 417.6860046386719
+ },
+ {
+ "x": 552.2020263671875,
+ "y": 410.41900634765625
+ },
+ {
+ "x": 544.0449829101562,
+ "y": 402.6820068359375
+ },
+ {
+ "x": 536.3359985351562,
+ "y": 394.5
+ }
+ ],
+ "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": 959,
+ "y": 46.76300048828125
+ },
+ {
+ "x": 977.8519897460938,
+ "y": 50.20899963378906
+ },
+ {
+ "x": 996.2880249023438,
+ "y": 55.44499969482422
+ },
+ {
+ "x": 1014.1380004882812,
+ "y": 62.41999816894531
+ },
+ {
+ "x": 1031.238037109375,
+ "y": 71.0719985961914
+ },
+ {
+ "x": 1047.4320068359375,
+ "y": 81.3219985961914
+ },
+ {
+ "x": 1062.571044921875,
+ "y": 93.0739974975586
+ },
+ {
+ "x": 1076.5150146484375,
+ "y": 106.22100067138672
+ },
+ {
+ "x": 1089.136962890625,
+ "y": 120.64199829101562
+ },
+ {
+ "x": 1100.3199462890625,
+ "y": 136.2050018310547
+ },
+ {
+ "x": 1109.9630126953125,
+ "y": 152.76800537109375
+ },
+ {
+ "x": 1117.9759521484375,
+ "y": 170.177001953125
+ },
+ {
+ "x": 1124.2860107421875,
+ "y": 188.2729949951172
+ },
+ {
+ "x": 1128.8349609375,
+ "y": 206.88999938964844
+ },
+ {
+ "x": 1131.5810546875,
+ "y": 225.85699462890625
+ },
+ {
+ "x": 1132.5,
+ "y": 245
+ },
+ {
+ "x": 1131.5810546875,
+ "y": 264.1419982910156
+ },
+ {
+ "x": 1128.8349609375,
+ "y": 283.1090087890625
+ },
+ {
+ "x": 1124.2860107421875,
+ "y": 301.72601318359375
+ },
+ {
+ "x": 1117.9759521484375,
+ "y": 319.8219909667969
+ },
+ {
+ "x": 1109.9630126953125,
+ "y": 337.2309875488281
+ },
+ {
+ "x": 1100.3199462890625,
+ "y": 353.79400634765625
+ },
+ {
+ "x": 1089.136962890625,
+ "y": 369.35699462890625
+ },
+ {
+ "x": 1076.5150146484375,
+ "y": 383.77801513671875
+ },
+ {
+ "x": 1062.571044921875,
+ "y": 396.92498779296875
+ },
+ {
+ "x": 1047.4320068359375,
+ "y": 408.677001953125
+ },
+ {
+ "x": 1031.238037109375,
+ "y": 418.927001953125
+ },
+ {
+ "x": 1014.1380004882812,
+ "y": 427.5790100097656
+ },
+ {
+ "x": 996.2880249023438,
+ "y": 434.5539855957031
+ },
+ {
+ "x": 977.8519897460938,
+ "y": 439.7900085449219
+ },
+ {
+ "x": 959,
+ "y": 443.2359924316406
+ }
+ ],
+ "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/isometric.exp.svg b/e2etests/testdata/txtar/cycle-diagram/elk/isometric.exp.svg
new file mode 100644
index 0000000000..96aef41957
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/isometric.exp.svg
@@ -0,0 +1 @@
+D2 isometric diagram
\ No newline at end of file
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..a25815cecc
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
@@ -0,0 +1,29 @@
+abcdabcab
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json b/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json
new file mode 100644
index 0000000000..befb9fd6d0
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/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": 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": 200,
+ "y": 0
+ },
+ "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": 400,
+ "y": 200
+ },
+ "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": 200,
+ "y": 400
+ },
+ "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": 0,
+ "y": 200
+ },
+ "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": 474,
+ "y": 0
+ },
+ "width": 400,
+ "height": 433,
+ "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": 647,
+ "y": 0
+ },
+ "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": 820,
+ "y": 300
+ },
+ "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": 474,
+ "y": 300
+ },
+ "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": 894,
+ "y": 0
+ },
+ "width": 227,
+ "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": 894,
+ "y": 0
+ },
+ "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": 894,
+ "y": 400
+ },
+ "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": 253.5,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 261.8789978027344,
+ "y": 36.06399917602539
+ },
+ {
+ "x": 270.1969909667969,
+ "y": 37.720001220703125
+ },
+ {
+ "x": 278.4360046386719,
+ "y": 39.72700119018555
+ },
+ {
+ "x": 286.5830078125,
+ "y": 42.08100128173828
+ },
+ {
+ "x": 294.62298583984375,
+ "y": 44.77899932861328
+ },
+ {
+ "x": 302.5409851074219,
+ "y": 47.814998626708984
+ },
+ {
+ "x": 310.3240051269531,
+ "y": 51.18299865722656
+ },
+ {
+ "x": 317.95599365234375,
+ "y": 54.87900161743164
+ },
+ {
+ "x": 325.4259948730469,
+ "y": 58.89500045776367
+ },
+ {
+ "x": 332.7179870605469,
+ "y": 63.2239990234375
+ },
+ {
+ "x": 339.82000732421875,
+ "y": 67.85800170898438
+ },
+ {
+ "x": 346.718994140625,
+ "y": 72.79000091552734
+ },
+ {
+ "x": 353.40301513671875,
+ "y": 78.00900268554688
+ },
+ {
+ "x": 359.8599853515625,
+ "y": 83.50700378417969
+ },
+ {
+ "x": 366.0780029296875,
+ "y": 89.27300262451172
+ },
+ {
+ "x": 372.0459899902344,
+ "y": 95.2979965209961
+ },
+ {
+ "x": 377.75299072265625,
+ "y": 101.57099914550781
+ },
+ {
+ "x": 383.1889953613281,
+ "y": 108.08000183105469
+ },
+ {
+ "x": 388.343994140625,
+ "y": 114.81300354003906
+ },
+ {
+ "x": 393.2090148925781,
+ "y": 121.75900268554688
+ },
+ {
+ "x": 397.7749938964844,
+ "y": 128.90499877929688
+ },
+ {
+ "x": 402.0350036621094,
+ "y": 136.23800659179688
+ },
+ {
+ "x": 405.97900390625,
+ "y": 143.74600219726562
+ },
+ {
+ "x": 409.60198974609375,
+ "y": 151.41299438476562
+ },
+ {
+ "x": 412.89599609375,
+ "y": 159.2270050048828
+ },
+ {
+ "x": 415.85699462890625,
+ "y": 167.1739959716797
+ },
+ {
+ "x": 418.47698974609375,
+ "y": 175.24000549316406
+ },
+ {
+ "x": 420.7539978027344,
+ "y": 183.40899658203125
+ },
+ {
+ "x": 422.6820068359375,
+ "y": 191.66700744628906
+ },
+ {
+ "x": 424.25799560546875,
+ "y": 200
+ }
+ ],
+ "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": 424.25799560546875,
+ "y": 266
+ },
+ {
+ "x": 422.6820068359375,
+ "y": 274.3320007324219
+ },
+ {
+ "x": 420.7539978027344,
+ "y": 282.5899963378906
+ },
+ {
+ "x": 418.47698974609375,
+ "y": 290.7590026855469
+ },
+ {
+ "x": 415.85699462890625,
+ "y": 298.82501220703125
+ },
+ {
+ "x": 412.89599609375,
+ "y": 306.7720031738281
+ },
+ {
+ "x": 409.60198974609375,
+ "y": 314.58599853515625
+ },
+ {
+ "x": 405.97900390625,
+ "y": 322.25299072265625
+ },
+ {
+ "x": 402.0350036621094,
+ "y": 329.760986328125
+ },
+ {
+ "x": 397.7749938964844,
+ "y": 337.093994140625
+ },
+ {
+ "x": 393.2090148925781,
+ "y": 344.239990234375
+ },
+ {
+ "x": 388.343994140625,
+ "y": 351.1860046386719
+ },
+ {
+ "x": 383.1889953613281,
+ "y": 357.91900634765625
+ },
+ {
+ "x": 377.75299072265625,
+ "y": 364.4280090332031
+ },
+ {
+ "x": 372.0459899902344,
+ "y": 370.70098876953125
+ },
+ {
+ "x": 366.0780029296875,
+ "y": 376.72601318359375
+ },
+ {
+ "x": 359.8599853515625,
+ "y": 382.49200439453125
+ },
+ {
+ "x": 353.40301513671875,
+ "y": 387.989990234375
+ },
+ {
+ "x": 346.718994140625,
+ "y": 393.2090148925781
+ },
+ {
+ "x": 339.82000732421875,
+ "y": 398.1409912109375
+ },
+ {
+ "x": 332.7179870605469,
+ "y": 402.7749938964844
+ },
+ {
+ "x": 325.4259948730469,
+ "y": 407.10400390625
+ },
+ {
+ "x": 317.95599365234375,
+ "y": 411.1199951171875
+ },
+ {
+ "x": 310.3240051269531,
+ "y": 414.8160095214844
+ },
+ {
+ "x": 302.5409851074219,
+ "y": 418.1839904785156
+ },
+ {
+ "x": 294.62298583984375,
+ "y": 421.2200012207031
+ },
+ {
+ "x": 286.5830078125,
+ "y": 423.9179992675781
+ },
+ {
+ "x": 278.4360046386719,
+ "y": 426.2720031738281
+ },
+ {
+ "x": 270.1969909667969,
+ "y": 428.27899169921875
+ },
+ {
+ "x": 261.8789978027344,
+ "y": 429.93499755859375
+ },
+ {
+ "x": 253.5,
+ "y": 431.2359924316406
+ }
+ ],
+ "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": 200.5,
+ "y": 431.2359924316406
+ },
+ {
+ "x": 192.1199951171875,
+ "y": 429.93499755859375
+ },
+ {
+ "x": 183.802001953125,
+ "y": 428.27899169921875
+ },
+ {
+ "x": 175.56300354003906,
+ "y": 426.2720031738281
+ },
+ {
+ "x": 167.41600036621094,
+ "y": 423.9179992675781
+ },
+ {
+ "x": 159.37600708007812,
+ "y": 421.2200012207031
+ },
+ {
+ "x": 151.45799255371094,
+ "y": 418.1839904785156
+ },
+ {
+ "x": 143.6750030517578,
+ "y": 414.8160095214844
+ },
+ {
+ "x": 136.04299926757812,
+ "y": 411.1199951171875
+ },
+ {
+ "x": 128.572998046875,
+ "y": 407.10400390625
+ },
+ {
+ "x": 121.28099822998047,
+ "y": 402.7749938964844
+ },
+ {
+ "x": 114.17900085449219,
+ "y": 398.1409912109375
+ },
+ {
+ "x": 107.27999877929688,
+ "y": 393.2090148925781
+ },
+ {
+ "x": 100.59600067138672,
+ "y": 387.989990234375
+ },
+ {
+ "x": 94.13899993896484,
+ "y": 382.49200439453125
+ },
+ {
+ "x": 87.9209976196289,
+ "y": 376.72601318359375
+ },
+ {
+ "x": 81.9530029296875,
+ "y": 370.70098876953125
+ },
+ {
+ "x": 76.24600219726562,
+ "y": 364.4280090332031
+ },
+ {
+ "x": 70.80999755859375,
+ "y": 357.91900634765625
+ },
+ {
+ "x": 65.65499877929688,
+ "y": 351.1860046386719
+ },
+ {
+ "x": 60.790000915527344,
+ "y": 344.239990234375
+ },
+ {
+ "x": 56.2239990234375,
+ "y": 337.093994140625
+ },
+ {
+ "x": 51.9640007019043,
+ "y": 329.760986328125
+ },
+ {
+ "x": 48.02000045776367,
+ "y": 322.25299072265625
+ },
+ {
+ "x": 44.39699935913086,
+ "y": 314.58599853515625
+ },
+ {
+ "x": 41.10300064086914,
+ "y": 306.7720031738281
+ },
+ {
+ "x": 38.141998291015625,
+ "y": 298.82501220703125
+ },
+ {
+ "x": 35.52199935913086,
+ "y": 290.7590026855469
+ },
+ {
+ "x": 33.244998931884766,
+ "y": 282.5899963378906
+ },
+ {
+ "x": 31.316999435424805,
+ "y": 274.3320007324219
+ },
+ {
+ "x": 29.740999221801758,
+ "y": 266
+ }
+ ],
+ "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": 700.2050170898438,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 711.9099731445312,
+ "y": 36.68299865722656
+ },
+ {
+ "x": 723.4810180664062,
+ "y": 39.292999267578125
+ },
+ {
+ "x": 734.8770141601562,
+ "y": 42.58399963378906
+ },
+ {
+ "x": 746.0570068359375,
+ "y": 46.54600143432617
+ },
+ {
+ "x": 756.9829711914062,
+ "y": 51.16299819946289
+ },
+ {
+ "x": 767.6170043945312,
+ "y": 56.41899871826172
+ },
+ {
+ "x": 777.9199829101562,
+ "y": 62.297000885009766
+ },
+ {
+ "x": 787.8560180664062,
+ "y": 68.7760009765625
+ },
+ {
+ "x": 797.3909912109375,
+ "y": 75.83200073242188
+ },
+ {
+ "x": 806.489990234375,
+ "y": 83.44000244140625
+ },
+ {
+ "x": 815.1229858398438,
+ "y": 91.57499694824219
+ },
+ {
+ "x": 823.2579956054688,
+ "y": 100.20800018310547
+ },
+ {
+ "x": 830.8670043945312,
+ "y": 109.30699920654297
+ },
+ {
+ "x": 837.9240112304688,
+ "y": 118.84200286865234
+ },
+ {
+ "x": 844.4030151367188,
+ "y": 128.7779998779297
+ },
+ {
+ "x": 850.281005859375,
+ "y": 139.0800018310547
+ },
+ {
+ "x": 855.5380249023438,
+ "y": 149.71299743652344
+ },
+ {
+ "x": 860.156005859375,
+ "y": 160.63900756835938
+ },
+ {
+ "x": 864.1170043945312,
+ "y": 171.82000732421875
+ },
+ {
+ "x": 867.4089965820312,
+ "y": 183.21499633789062
+ },
+ {
+ "x": 870.02001953125,
+ "y": 194.78599548339844
+ },
+ {
+ "x": 871.9400024414062,
+ "y": 206.49200439453125
+ },
+ {
+ "x": 873.1630249023438,
+ "y": 218.2899932861328
+ },
+ {
+ "x": 873.6840209960938,
+ "y": 230.13999938964844
+ },
+ {
+ "x": 873.5020141601562,
+ "y": 242
+ },
+ {
+ "x": 872.6170043945312,
+ "y": 253.82899475097656
+ },
+ {
+ "x": 871.031982421875,
+ "y": 265.5840148925781
+ },
+ {
+ "x": 868.7540283203125,
+ "y": 277.2250061035156
+ },
+ {
+ "x": 865.7890014648438,
+ "y": 288.7099914550781
+ },
+ {
+ "x": 862.1480102539062,
+ "y": 300
+ }
+ ],
+ "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": 823.072998046875,
+ "y": 366
+ },
+ {
+ "x": 815.364013671875,
+ "y": 374.1820068359375
+ },
+ {
+ "x": 807.2069702148438,
+ "y": 381.91900634765625
+ },
+ {
+ "x": 798.6279907226562,
+ "y": 389.1860046386719
+ },
+ {
+ "x": 789.655029296875,
+ "y": 395.9580078125
+ },
+ {
+ "x": 780.3150024414062,
+ "y": 402.21600341796875
+ },
+ {
+ "x": 770.6380004882812,
+ "y": 407.9389953613281
+ },
+ {
+ "x": 760.655029296875,
+ "y": 413.1099853515625
+ },
+ {
+ "x": 750.39697265625,
+ "y": 417.71099853515625
+ },
+ {
+ "x": 739.89697265625,
+ "y": 421.7279968261719
+ },
+ {
+ "x": 729.18701171875,
+ "y": 425.1499938964844
+ },
+ {
+ "x": 718.302978515625,
+ "y": 427.9639892578125
+ },
+ {
+ "x": 707.2769775390625,
+ "y": 430.1619873046875
+ },
+ {
+ "x": 696.14501953125,
+ "y": 431.73699951171875
+ },
+ {
+ "x": 684.9429931640625,
+ "y": 432.6839904785156
+ },
+ {
+ "x": 673.7050170898438,
+ "y": 433
+ },
+ {
+ "x": 662.4660034179688,
+ "y": 432.6839904785156
+ },
+ {
+ "x": 651.2639770507812,
+ "y": 431.73699951171875
+ },
+ {
+ "x": 640.1320190429688,
+ "y": 430.1619873046875
+ },
+ {
+ "x": 629.1069946289062,
+ "y": 427.9639892578125
+ },
+ {
+ "x": 618.2219848632812,
+ "y": 425.1499938964844
+ },
+ {
+ "x": 607.5130004882812,
+ "y": 421.7279968261719
+ },
+ {
+ "x": 597.0120239257812,
+ "y": 417.71099853515625
+ },
+ {
+ "x": 586.7540283203125,
+ "y": 413.1099853515625
+ },
+ {
+ "x": 576.77099609375,
+ "y": 407.9389953613281
+ },
+ {
+ "x": 567.094970703125,
+ "y": 402.21600341796875
+ },
+ {
+ "x": 557.7550048828125,
+ "y": 395.9580078125
+ },
+ {
+ "x": 548.781005859375,
+ "y": 389.1860046386719
+ },
+ {
+ "x": 540.2020263671875,
+ "y": 381.91900634765625
+ },
+ {
+ "x": 532.0449829101562,
+ "y": 374.1820068359375
+ },
+ {
+ "x": 524.3359985351562,
+ "y": 366
+ }
+ ],
+ "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": 947,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 965.8519897460938,
+ "y": 38.20899963378906
+ },
+ {
+ "x": 984.2880249023438,
+ "y": 43.44499969482422
+ },
+ {
+ "x": 1002.1380004882812,
+ "y": 50.41999816894531
+ },
+ {
+ "x": 1019.2379760742188,
+ "y": 59.071998596191406
+ },
+ {
+ "x": 1035.4320068359375,
+ "y": 69.3219985961914
+ },
+ {
+ "x": 1050.571044921875,
+ "y": 81.0739974975586
+ },
+ {
+ "x": 1064.5150146484375,
+ "y": 94.22100067138672
+ },
+ {
+ "x": 1077.136962890625,
+ "y": 108.64199829101562
+ },
+ {
+ "x": 1088.3199462890625,
+ "y": 124.20500183105469
+ },
+ {
+ "x": 1097.9630126953125,
+ "y": 140.76800537109375
+ },
+ {
+ "x": 1105.9759521484375,
+ "y": 158.177001953125
+ },
+ {
+ "x": 1112.2860107421875,
+ "y": 176.2729949951172
+ },
+ {
+ "x": 1116.8349609375,
+ "y": 194.88999938964844
+ },
+ {
+ "x": 1119.5810546875,
+ "y": 213.85699462890625
+ },
+ {
+ "x": 1120.5,
+ "y": 233
+ },
+ {
+ "x": 1119.5810546875,
+ "y": 252.14199829101562
+ },
+ {
+ "x": 1116.8349609375,
+ "y": 271.1090087890625
+ },
+ {
+ "x": 1112.2860107421875,
+ "y": 289.72601318359375
+ },
+ {
+ "x": 1105.9759521484375,
+ "y": 307.8219909667969
+ },
+ {
+ "x": 1097.9630126953125,
+ "y": 325.2309875488281
+ },
+ {
+ "x": 1088.3199462890625,
+ "y": 341.79400634765625
+ },
+ {
+ "x": 1077.136962890625,
+ "y": 357.35699462890625
+ },
+ {
+ "x": 1064.5150146484375,
+ "y": 371.77801513671875
+ },
+ {
+ "x": 1050.571044921875,
+ "y": 384.92498779296875
+ },
+ {
+ "x": 1035.4320068359375,
+ "y": 396.677001953125
+ },
+ {
+ "x": 1019.2379760742188,
+ "y": 406.927001953125
+ },
+ {
+ "x": 1002.1380004882812,
+ "y": 415.5790100097656
+ },
+ {
+ "x": 984.2880249023438,
+ "y": 422.5539855957031
+ },
+ {
+ "x": 965.8519897460938,
+ "y": 427.7900085449219
+ },
+ {
+ "x": 947,
+ "y": 431.2359924316406
+ }
+ ],
+ "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/tala/isometric.exp.svg b/e2etests/testdata/txtar/cycle-diagram/tala/isometric.exp.svg
new file mode 100644
index 0000000000..3898c33b80
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/isometric.exp.svg
@@ -0,0 +1 @@
+D2 isometric diagram
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
new file mode 100644
index 0000000000..acf45b9da1
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
@@ -0,0 +1,29 @@
+abcdabcab
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/dagre/board.exp.json b/e2etests/testdata/txtar/cycle-self-loop/dagre/board.exp.json
new file mode 100644
index 0000000000..dceeba481b
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/dagre/board.exp.json
@@ -0,0 +1,735 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "x",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 227,
+ "height": 506,
+ "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": "x.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 40
+ },
+ "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": "x.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 440
+ },
+ "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": "y",
+ "type": "cycle",
+ "pos": {
+ "x": 287,
+ "y": 200
+ },
+ "width": 53,
+ "height": 106,
+ "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": "y.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 287,
+ "y": 240
+ },
+ "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
+ }
+ ],
+ "connections": [
+ {
+ "id": "x.(a -> a)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.a",
+ "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": 40
+ },
+ {
+ "x": 22.340999603271484,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 18.364999771118164,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 14.744000434875488,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 11.63700008392334,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 9.178999900817871,
+ "y": 30
+ },
+ {
+ "x": 7.478000164031982,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 6.609000205993652,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 6.609000205993652,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 7.478000164031982,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 9.178999900817871,
+ "y": 10
+ },
+ {
+ "x": 11.63700008392334,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 14.744000434875488,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 18.364999771118164,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 22.340999603271484,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 26.499000549316406,
+ "y": 0
+ },
+ {
+ "x": 30.658000946044922,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 34.63399887084961,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 38.255001068115234,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 41.36199951171875,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 43.81999969482422,
+ "y": 10
+ },
+ {
+ "x": 45.520999908447266,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 46.38999938964844,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 46.38999938964844,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 45.520999908447266,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 43.81999969482422,
+ "y": 30
+ },
+ {
+ "x": 41.36199951171875,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 38.255001068115234,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 34.63399887084961,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 30.658000946044922,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 26.5,
+ "y": 40
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "x.(a -> b)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.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": 53,
+ "y": 74.76300048828125
+ },
+ {
+ "x": 71.85199737548828,
+ "y": 78.20899963378906
+ },
+ {
+ "x": 90.28800201416016,
+ "y": 83.44499969482422
+ },
+ {
+ "x": 108.13800048828125,
+ "y": 90.41999816894531
+ },
+ {
+ "x": 125.23799896240234,
+ "y": 99.0719985961914
+ },
+ {
+ "x": 141.4320068359375,
+ "y": 109.3219985961914
+ },
+ {
+ "x": 156.5709991455078,
+ "y": 121.0739974975586
+ },
+ {
+ "x": 170.51499938964844,
+ "y": 134.2209930419922
+ },
+ {
+ "x": 183.13699340820312,
+ "y": 148.64199829101562
+ },
+ {
+ "x": 194.32000732421875,
+ "y": 164.2050018310547
+ },
+ {
+ "x": 203.96299743652344,
+ "y": 180.76800537109375
+ },
+ {
+ "x": 211.9759979248047,
+ "y": 198.177001953125
+ },
+ {
+ "x": 218.28599548339844,
+ "y": 216.2729949951172
+ },
+ {
+ "x": 222.8350067138672,
+ "y": 234.88999938964844
+ },
+ {
+ "x": 225.58099365234375,
+ "y": 253.85699462890625
+ },
+ {
+ "x": 226.5,
+ "y": 273
+ },
+ {
+ "x": 225.58099365234375,
+ "y": 292.1419982910156
+ },
+ {
+ "x": 222.8350067138672,
+ "y": 311.1090087890625
+ },
+ {
+ "x": 218.28599548339844,
+ "y": 329.72601318359375
+ },
+ {
+ "x": 211.9759979248047,
+ "y": 347.8219909667969
+ },
+ {
+ "x": 203.96299743652344,
+ "y": 365.2309875488281
+ },
+ {
+ "x": 194.32000732421875,
+ "y": 381.79400634765625
+ },
+ {
+ "x": 183.13699340820312,
+ "y": 397.35699462890625
+ },
+ {
+ "x": 170.51499938964844,
+ "y": 411.77801513671875
+ },
+ {
+ "x": 156.5709991455078,
+ "y": 424.92498779296875
+ },
+ {
+ "x": 141.4320068359375,
+ "y": 436.677001953125
+ },
+ {
+ "x": 125.23799896240234,
+ "y": 446.927001953125
+ },
+ {
+ "x": 108.13800048828125,
+ "y": 455.5790100097656
+ },
+ {
+ "x": 90.28800201416016,
+ "y": 462.5539855957031
+ },
+ {
+ "x": 71.85199737548828,
+ "y": 467.7900085449219
+ },
+ {
+ "x": 53,
+ "y": 471.2359924316406
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "y.(a -> a)[0]",
+ "src": "y.a",
+ "srcArrow": "none",
+ "dst": "y.a",
+ "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": 313.5,
+ "y": 240
+ },
+ {
+ "x": 309.34100341796875,
+ "y": 239.56199645996094
+ },
+ {
+ "x": 305.364990234375,
+ "y": 238.27000427246094
+ },
+ {
+ "x": 301.7439880371094,
+ "y": 236.17999267578125
+ },
+ {
+ "x": 298.6369934082031,
+ "y": 233.3820037841797
+ },
+ {
+ "x": 296.1789855957031,
+ "y": 230
+ },
+ {
+ "x": 294.4779968261719,
+ "y": 226.17999267578125
+ },
+ {
+ "x": 293.6090087890625,
+ "y": 222.08999633789062
+ },
+ {
+ "x": 293.6090087890625,
+ "y": 217.90899658203125
+ },
+ {
+ "x": 294.4779968261719,
+ "y": 213.81900024414062
+ },
+ {
+ "x": 296.1789855957031,
+ "y": 210
+ },
+ {
+ "x": 298.6369934082031,
+ "y": 206.61700439453125
+ },
+ {
+ "x": 301.7439880371094,
+ "y": 203.81900024414062
+ },
+ {
+ "x": 305.364990234375,
+ "y": 201.72900390625
+ },
+ {
+ "x": 309.34100341796875,
+ "y": 200.43699645996094
+ },
+ {
+ "x": 313.5,
+ "y": 200
+ },
+ {
+ "x": 317.6579895019531,
+ "y": 200.43699645996094
+ },
+ {
+ "x": 321.6340026855469,
+ "y": 201.72900390625
+ },
+ {
+ "x": 325.2550048828125,
+ "y": 203.81900024414062
+ },
+ {
+ "x": 328.36199951171875,
+ "y": 206.61700439453125
+ },
+ {
+ "x": 330.82000732421875,
+ "y": 210
+ },
+ {
+ "x": 332.52099609375,
+ "y": 213.81900024414062
+ },
+ {
+ "x": 333.3900146484375,
+ "y": 217.90899658203125
+ },
+ {
+ "x": 333.3900146484375,
+ "y": 222.08999633789062
+ },
+ {
+ "x": 332.52099609375,
+ "y": 226.17999267578125
+ },
+ {
+ "x": 330.82000732421875,
+ "y": 230
+ },
+ {
+ "x": 328.36199951171875,
+ "y": 233.3820037841797
+ },
+ {
+ "x": 325.2550048828125,
+ "y": 236.17999267578125
+ },
+ {
+ "x": 321.6340026855469,
+ "y": 238.27000427246094
+ },
+ {
+ "x": 317.6579895019531,
+ "y": 239.56199645996094
+ },
+ {
+ "x": 313.5,
+ "y": 240
+ }
+ ],
+ "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-self-loop/dagre/isometric.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/dagre/isometric.exp.svg
new file mode 100644
index 0000000000..823eae6d5b
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/dagre/isometric.exp.svg
@@ -0,0 +1 @@
+D2 isometric diagram
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/dagre/sketch.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/dagre/sketch.exp.svg
new file mode 100644
index 0000000000..363bcaadc7
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/dagre/sketch.exp.svg
@@ -0,0 +1,29 @@
+aba
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/elk/board.exp.json b/e2etests/testdata/txtar/cycle-self-loop/elk/board.exp.json
new file mode 100644
index 0000000000..4c52a7c163
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/elk/board.exp.json
@@ -0,0 +1,735 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "x",
+ "type": "cycle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 227,
+ "height": 506,
+ "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": "x.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 52
+ },
+ "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": "x.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 452
+ },
+ "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": "y",
+ "type": "cycle",
+ "pos": {
+ "x": 259,
+ "y": 212
+ },
+ "width": 53,
+ "height": 106,
+ "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": "y.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 259,
+ "y": 252
+ },
+ "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
+ }
+ ],
+ "connections": [
+ {
+ "id": "x.(a -> a)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.a",
+ "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": 52
+ },
+ {
+ "x": 34.340999603271484,
+ "y": 51.5620002746582
+ },
+ {
+ "x": 30.364999771118164,
+ "y": 50.27000045776367
+ },
+ {
+ "x": 26.743999481201172,
+ "y": 48.18000030517578
+ },
+ {
+ "x": 23.636999130249023,
+ "y": 45.38199996948242
+ },
+ {
+ "x": 21.179000854492188,
+ "y": 42
+ },
+ {
+ "x": 19.47800064086914,
+ "y": 38.18000030517578
+ },
+ {
+ "x": 18.608999252319336,
+ "y": 34.09000015258789
+ },
+ {
+ "x": 18.608999252319336,
+ "y": 29.909000396728516
+ },
+ {
+ "x": 19.47800064086914,
+ "y": 25.819000244140625
+ },
+ {
+ "x": 21.179000854492188,
+ "y": 22
+ },
+ {
+ "x": 23.636999130249023,
+ "y": 18.617000579833984
+ },
+ {
+ "x": 26.743999481201172,
+ "y": 15.819000244140625
+ },
+ {
+ "x": 30.364999771118164,
+ "y": 13.729000091552734
+ },
+ {
+ "x": 34.340999603271484,
+ "y": 12.437000274658203
+ },
+ {
+ "x": 38.5,
+ "y": 12
+ },
+ {
+ "x": 42.65800094604492,
+ "y": 12.437000274658203
+ },
+ {
+ "x": 46.63399887084961,
+ "y": 13.729000091552734
+ },
+ {
+ "x": 50.255001068115234,
+ "y": 15.819000244140625
+ },
+ {
+ "x": 53.36199951171875,
+ "y": 18.617000579833984
+ },
+ {
+ "x": 55.81999969482422,
+ "y": 22
+ },
+ {
+ "x": 57.520999908447266,
+ "y": 25.819000244140625
+ },
+ {
+ "x": 58.38999938964844,
+ "y": 29.909000396728516
+ },
+ {
+ "x": 58.38999938964844,
+ "y": 34.09000015258789
+ },
+ {
+ "x": 57.520999908447266,
+ "y": 38.18000030517578
+ },
+ {
+ "x": 55.81999969482422,
+ "y": 42
+ },
+ {
+ "x": 53.36199951171875,
+ "y": 45.38199996948242
+ },
+ {
+ "x": 50.255001068115234,
+ "y": 48.18000030517578
+ },
+ {
+ "x": 46.63399887084961,
+ "y": 50.27000045776367
+ },
+ {
+ "x": 42.65800094604492,
+ "y": 51.5620002746582
+ },
+ {
+ "x": 38.5,
+ "y": 52
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "x.(a -> b)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.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": 65,
+ "y": 86.76300048828125
+ },
+ {
+ "x": 83.85199737548828,
+ "y": 90.20899963378906
+ },
+ {
+ "x": 102.28800201416016,
+ "y": 95.44499969482422
+ },
+ {
+ "x": 120.13800048828125,
+ "y": 102.41999816894531
+ },
+ {
+ "x": 137.23800659179688,
+ "y": 111.0719985961914
+ },
+ {
+ "x": 153.4320068359375,
+ "y": 121.3219985961914
+ },
+ {
+ "x": 168.5709991455078,
+ "y": 133.07400512695312
+ },
+ {
+ "x": 182.51499938964844,
+ "y": 146.2209930419922
+ },
+ {
+ "x": 195.13699340820312,
+ "y": 160.64199829101562
+ },
+ {
+ "x": 206.32000732421875,
+ "y": 176.2050018310547
+ },
+ {
+ "x": 215.96299743652344,
+ "y": 192.76800537109375
+ },
+ {
+ "x": 223.9759979248047,
+ "y": 210.177001953125
+ },
+ {
+ "x": 230.28599548339844,
+ "y": 228.2729949951172
+ },
+ {
+ "x": 234.8350067138672,
+ "y": 246.88999938964844
+ },
+ {
+ "x": 237.58099365234375,
+ "y": 265.85699462890625
+ },
+ {
+ "x": 238.5,
+ "y": 285
+ },
+ {
+ "x": 237.58099365234375,
+ "y": 304.1419982910156
+ },
+ {
+ "x": 234.8350067138672,
+ "y": 323.1090087890625
+ },
+ {
+ "x": 230.28599548339844,
+ "y": 341.72601318359375
+ },
+ {
+ "x": 223.9759979248047,
+ "y": 359.8219909667969
+ },
+ {
+ "x": 215.96299743652344,
+ "y": 377.2309875488281
+ },
+ {
+ "x": 206.32000732421875,
+ "y": 393.79400634765625
+ },
+ {
+ "x": 195.13699340820312,
+ "y": 409.35699462890625
+ },
+ {
+ "x": 182.51499938964844,
+ "y": 423.77801513671875
+ },
+ {
+ "x": 168.5709991455078,
+ "y": 436.92498779296875
+ },
+ {
+ "x": 153.4320068359375,
+ "y": 448.677001953125
+ },
+ {
+ "x": 137.23800659179688,
+ "y": 458.927001953125
+ },
+ {
+ "x": 120.13800048828125,
+ "y": 467.5790100097656
+ },
+ {
+ "x": 102.28800201416016,
+ "y": 474.5539855957031
+ },
+ {
+ "x": 83.85199737548828,
+ "y": 479.7900085449219
+ },
+ {
+ "x": 65,
+ "y": 483.2359924316406
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "y.(a -> a)[0]",
+ "src": "y.a",
+ "srcArrow": "none",
+ "dst": "y.a",
+ "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": 285.5,
+ "y": 252
+ },
+ {
+ "x": 281.34100341796875,
+ "y": 251.56199645996094
+ },
+ {
+ "x": 277.364990234375,
+ "y": 250.27000427246094
+ },
+ {
+ "x": 273.7439880371094,
+ "y": 248.17999267578125
+ },
+ {
+ "x": 270.6369934082031,
+ "y": 245.3820037841797
+ },
+ {
+ "x": 268.1789855957031,
+ "y": 242
+ },
+ {
+ "x": 266.4779968261719,
+ "y": 238.17999267578125
+ },
+ {
+ "x": 265.6090087890625,
+ "y": 234.08999633789062
+ },
+ {
+ "x": 265.6090087890625,
+ "y": 229.90899658203125
+ },
+ {
+ "x": 266.4779968261719,
+ "y": 225.81900024414062
+ },
+ {
+ "x": 268.1789855957031,
+ "y": 222
+ },
+ {
+ "x": 270.6369934082031,
+ "y": 218.61700439453125
+ },
+ {
+ "x": 273.7439880371094,
+ "y": 215.81900024414062
+ },
+ {
+ "x": 277.364990234375,
+ "y": 213.72900390625
+ },
+ {
+ "x": 281.34100341796875,
+ "y": 212.43699645996094
+ },
+ {
+ "x": 285.5,
+ "y": 212
+ },
+ {
+ "x": 289.6579895019531,
+ "y": 212.43699645996094
+ },
+ {
+ "x": 293.6340026855469,
+ "y": 213.72900390625
+ },
+ {
+ "x": 297.2550048828125,
+ "y": 215.81900024414062
+ },
+ {
+ "x": 300.36199951171875,
+ "y": 218.61700439453125
+ },
+ {
+ "x": 302.82000732421875,
+ "y": 222
+ },
+ {
+ "x": 304.52099609375,
+ "y": 225.81900024414062
+ },
+ {
+ "x": 305.3900146484375,
+ "y": 229.90899658203125
+ },
+ {
+ "x": 305.3900146484375,
+ "y": 234.08999633789062
+ },
+ {
+ "x": 304.52099609375,
+ "y": 238.17999267578125
+ },
+ {
+ "x": 302.82000732421875,
+ "y": 242
+ },
+ {
+ "x": 300.36199951171875,
+ "y": 245.3820037841797
+ },
+ {
+ "x": 297.2550048828125,
+ "y": 248.17999267578125
+ },
+ {
+ "x": 293.6340026855469,
+ "y": 250.27000427246094
+ },
+ {
+ "x": 289.6579895019531,
+ "y": 251.56199645996094
+ },
+ {
+ "x": 285.5,
+ "y": 252
+ }
+ ],
+ "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-self-loop/elk/isometric.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/elk/isometric.exp.svg
new file mode 100644
index 0000000000..7fb5db723c
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/elk/isometric.exp.svg
@@ -0,0 +1 @@
+D2 isometric diagram
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/elk/sketch.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/elk/sketch.exp.svg
new file mode 100644
index 0000000000..86dc32353c
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/elk/sketch.exp.svg
@@ -0,0 +1,29 @@
+aba
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/tala/board.exp.json b/e2etests/testdata/txtar/cycle-self-loop/tala/board.exp.json
new file mode 100644
index 0000000000..7455468e4d
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/tala/board.exp.json
@@ -0,0 +1,735 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "x",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 227,
+ "height": 506,
+ "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": "x.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 40
+ },
+ "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": "x.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 440
+ },
+ "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": "y",
+ "type": "cycle",
+ "pos": {
+ "x": 247,
+ "y": 0
+ },
+ "width": 53,
+ "height": 106,
+ "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": "y.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 247,
+ "y": 40
+ },
+ "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
+ }
+ ],
+ "connections": [
+ {
+ "id": "x.(a -> a)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.a",
+ "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": 40
+ },
+ {
+ "x": 22.340999603271484,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 18.364999771118164,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 14.744000434875488,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 11.63700008392334,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 9.178999900817871,
+ "y": 30
+ },
+ {
+ "x": 7.478000164031982,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 6.609000205993652,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 6.609000205993652,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 7.478000164031982,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 9.178999900817871,
+ "y": 10
+ },
+ {
+ "x": 11.63700008392334,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 14.744000434875488,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 18.364999771118164,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 22.340999603271484,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 26.499000549316406,
+ "y": 0
+ },
+ {
+ "x": 30.658000946044922,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 34.63399887084961,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 38.255001068115234,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 41.36199951171875,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 43.81999969482422,
+ "y": 10
+ },
+ {
+ "x": 45.520999908447266,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 46.38999938964844,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 46.38999938964844,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 45.520999908447266,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 43.81999969482422,
+ "y": 30
+ },
+ {
+ "x": 41.36199951171875,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 38.255001068115234,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 34.63399887084961,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 30.658000946044922,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 26.5,
+ "y": 40
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "x.(a -> b)[0]",
+ "src": "x.a",
+ "srcArrow": "none",
+ "dst": "x.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": 53,
+ "y": 74.76300048828125
+ },
+ {
+ "x": 71.85199737548828,
+ "y": 78.20899963378906
+ },
+ {
+ "x": 90.28800201416016,
+ "y": 83.44499969482422
+ },
+ {
+ "x": 108.13800048828125,
+ "y": 90.41999816894531
+ },
+ {
+ "x": 125.23799896240234,
+ "y": 99.0719985961914
+ },
+ {
+ "x": 141.4320068359375,
+ "y": 109.3219985961914
+ },
+ {
+ "x": 156.5709991455078,
+ "y": 121.0739974975586
+ },
+ {
+ "x": 170.51499938964844,
+ "y": 134.2209930419922
+ },
+ {
+ "x": 183.13699340820312,
+ "y": 148.64199829101562
+ },
+ {
+ "x": 194.32000732421875,
+ "y": 164.2050018310547
+ },
+ {
+ "x": 203.96299743652344,
+ "y": 180.76800537109375
+ },
+ {
+ "x": 211.9759979248047,
+ "y": 198.177001953125
+ },
+ {
+ "x": 218.28599548339844,
+ "y": 216.2729949951172
+ },
+ {
+ "x": 222.8350067138672,
+ "y": 234.88999938964844
+ },
+ {
+ "x": 225.58099365234375,
+ "y": 253.85699462890625
+ },
+ {
+ "x": 226.5,
+ "y": 273
+ },
+ {
+ "x": 225.58099365234375,
+ "y": 292.1419982910156
+ },
+ {
+ "x": 222.8350067138672,
+ "y": 311.1090087890625
+ },
+ {
+ "x": 218.28599548339844,
+ "y": 329.72601318359375
+ },
+ {
+ "x": 211.9759979248047,
+ "y": 347.8219909667969
+ },
+ {
+ "x": 203.96299743652344,
+ "y": 365.2309875488281
+ },
+ {
+ "x": 194.32000732421875,
+ "y": 381.79400634765625
+ },
+ {
+ "x": 183.13699340820312,
+ "y": 397.35699462890625
+ },
+ {
+ "x": 170.51499938964844,
+ "y": 411.77801513671875
+ },
+ {
+ "x": 156.5709991455078,
+ "y": 424.92498779296875
+ },
+ {
+ "x": 141.4320068359375,
+ "y": 436.677001953125
+ },
+ {
+ "x": 125.23799896240234,
+ "y": 446.927001953125
+ },
+ {
+ "x": 108.13800048828125,
+ "y": 455.5790100097656
+ },
+ {
+ "x": 90.28800201416016,
+ "y": 462.5539855957031
+ },
+ {
+ "x": 71.85199737548828,
+ "y": 467.7900085449219
+ },
+ {
+ "x": 53,
+ "y": 471.2359924316406
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "y.(a -> a)[0]",
+ "src": "y.a",
+ "srcArrow": "none",
+ "dst": "y.a",
+ "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": 273.5,
+ "y": 40
+ },
+ {
+ "x": 269.34100341796875,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 265.364990234375,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 261.7439880371094,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 258.6369934082031,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 256.1789855957031,
+ "y": 30
+ },
+ {
+ "x": 254.47799682617188,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 253.60899353027344,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 253.60899353027344,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 254.47799682617188,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 256.1789855957031,
+ "y": 10
+ },
+ {
+ "x": 258.6369934082031,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 261.7439880371094,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 265.364990234375,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 269.34100341796875,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 273.5,
+ "y": 0
+ },
+ {
+ "x": 277.6579895019531,
+ "y": 0.43700000643730164
+ },
+ {
+ "x": 281.6340026855469,
+ "y": 1.7289999723434448
+ },
+ {
+ "x": 285.2550048828125,
+ "y": 3.819000005722046
+ },
+ {
+ "x": 288.36199951171875,
+ "y": 6.617000102996826
+ },
+ {
+ "x": 290.82000732421875,
+ "y": 10
+ },
+ {
+ "x": 292.52099609375,
+ "y": 13.819000244140625
+ },
+ {
+ "x": 293.3900146484375,
+ "y": 17.909000396728516
+ },
+ {
+ "x": 293.3900146484375,
+ "y": 22.09000015258789
+ },
+ {
+ "x": 292.52099609375,
+ "y": 26.18000030517578
+ },
+ {
+ "x": 290.82000732421875,
+ "y": 30
+ },
+ {
+ "x": 288.36199951171875,
+ "y": 33.38199996948242
+ },
+ {
+ "x": 285.2550048828125,
+ "y": 36.18000030517578
+ },
+ {
+ "x": 281.6340026855469,
+ "y": 38.27000045776367
+ },
+ {
+ "x": 277.6579895019531,
+ "y": 39.5620002746582
+ },
+ {
+ "x": 273.5,
+ "y": 40
+ }
+ ],
+ "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-self-loop/tala/isometric.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/tala/isometric.exp.svg
new file mode 100644
index 0000000000..1f96c315cc
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/tala/isometric.exp.svg
@@ -0,0 +1 @@
+D2 isometric diagram
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/cycle-self-loop/tala/sketch.exp.svg b/e2etests/testdata/txtar/cycle-self-loop/tala/sketch.exp.svg
new file mode 100644
index 0000000000..a31052ca40
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-self-loop/tala/sketch.exp.svg
@@ -0,0 +1,29 @@
+aba
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index 7585d91b3e..0083b67cfa 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -1773,3 +1773,27 @@ style: {fill-pattern: dots; fill:"radial-gradient(#fbfbf8, #e3e3f0)"; stroke: "#
a->b
+-- cycle-self-loop --
+x: "" {
+ shape: cycle
+ a -> a
+ a -> b
+}
+y: "" {
+ shape: cycle
+ a -> a
+}
+
+-- 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)
+}