diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index ffe01dbc1e..25a21db7ea 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,5 +1,8 @@
#### Features 🚀
+- layouts: add `shape: cycle` which arranges its children on a circle and routes
+ their edges along it as circular arcs trimmed to the children's borders
+
#### Improvements 🧹
- d2svg:
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/d2graph.go b/d2graph/d2graph.go
index 92f5c8948a..6c96306c5f 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -543,7 +543,7 @@ func (obj *Object) GetFill() string {
return color.N7
}
- if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) || strings.EqualFold(shape, d2target.ShapeHierarchy) {
+ if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) || strings.EqualFold(shape, d2target.ShapeCycleDiagram) || strings.EqualFold(shape, d2target.ShapeHierarchy) {
if level == 1 {
if !obj.IsContainer() {
return color.B6
diff --git a/d2layouts/d2cycle/layout.go b/d2layouts/d2cycle/layout.go
new file mode 100644
index 0000000000..ec48174369
--- /dev/null
+++ b/d2layouts/d2cycle/layout.go
@@ -0,0 +1,518 @@
+// Package d2cycle implements the layout for objects of shape cycle.
+//
+// Children of a cycle object are arranged with their centers on a circle and
+// edges between them are routed along that circle. Arcs are exact circular
+// curves (chains of cubics using the canonical k = 4/3·tan(Δθ/4) control
+// offset) whose endpoints are trimmed to each endpoint shape's visible border.
+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 (
+ // minRadius is the smallest radius a cycle is drawn with.
+ minRadius = 200.
+ // padding is the minimum gap between adjacent children along the circle.
+ padding = 24.
+ containerPadding = 30.
+ // maxArcSweep bounds each cubic segment so the Bezier approximation of a
+ // circular arc stays visually exact.
+ maxArcSweep = math.Pi / 2
+ // chordStep is the angular resolution used when intersecting the cycle
+ // circle with a child shape's perimeter. The sagitta of a 1° chord is
+ // ~R·2.4e-5, far below a pixel.
+ chordStep = math.Pi / 180
+ epsilon = 1e-6
+)
+
+// Layout arranges the direct children of the graph's root on a circle, keeps
+// the internal layout of every child intact, and routes root-level edges as
+// circular arcs trimmed to the endpoints' borders.
+func Layout(ctx context.Context, g *d2graph.Graph, coreLayout d2graph.LayoutGraph) error {
+ objects := g.Root.ChildrenArray
+ if len(objects) == 0 {
+ return nil
+ }
+
+ // Lay out children with the core engine first so nested content inside
+ // each child is fully positioned and routed before the child is moved
+ // onto the circle. The core layout may replace object pointers, so the
+ // children are re-read afterwards.
+ if coreLayout != nil {
+ if err := coreLayout(ctx, g); err != nil {
+ return err
+ }
+ }
+
+ positionLabelsIcons(g.Root)
+ for _, obj := range g.Objects {
+ positionLabelsIcons(obj)
+ }
+
+ radius := calculateRadius(g.Root.ChildrenArray)
+ if radius > 0 {
+ deltas := positionObjects(g.Root.ChildrenArray, radius)
+ applyDeltas(g, deltas)
+ directions := arcDirections(g)
+ for _, edge := range g.Edges {
+ if isCycleEdge(g, edge) {
+ routeCircularArc(edge, radius, directions[edge])
+ }
+ }
+ }
+
+ normalizeGraph(g)
+ return nil
+}
+
+// calculateRadius returns the circle radius that fits all children with
+// padding between neighbors, or 0 when there is nothing to place on a ring.
+func calculateRadius(objects []*d2graph.Object) float64 {
+ if len(objects) < 2 {
+ return 0
+ }
+
+ maxHalfDiagonal := 0.
+ for _, obj := range objects {
+ maxHalfDiagonal = math.Max(maxHalfDiagonal, math.Hypot(obj.Width/2, obj.Height/2))
+ }
+
+ minimum := (maxHalfDiagonal + padding) / math.Sin(math.Pi/float64(len(objects)))
+ return math.Max(minimum, minRadius)
+}
+
+// positionObjects centers each child on the circle, the first at 12 o'clock
+// and the rest clockwise, and returns the translation applied to each child.
+func positionObjects(objects []*d2graph.Object, radius float64) map[*d2graph.Object]*geo.Point {
+ deltas := make(map[*d2graph.Object]*geo.Point, len(objects))
+ for i, obj := range objects {
+ angle := -math.Pi/2 + 2*math.Pi*float64(i)/float64(len(objects))
+ x := radius * math.Cos(angle)
+ y := radius * math.Sin(angle)
+
+ var dx, dy float64
+ if obj.TopLeft != nil {
+ dx = x - obj.Width/2 - obj.TopLeft.X
+ dy = y - obj.Height/2 - obj.TopLeft.Y
+ }
+ deltas[obj] = geo.NewPoint(dx, dy)
+ }
+ return deltas
+}
+
+// applyDeltas moves every child together with its descendants and keeps all
+// edge routes attached to the moved endpoints.
+func applyDeltas(g *d2graph.Graph, deltas map[*d2graph.Object]*geo.Point) {
+ if len(deltas) == 0 {
+ return
+ }
+
+ for obj, delta := range deltas {
+ obj.MoveWithDescendants(delta.X, delta.Y)
+ }
+
+ for _, edge := range g.Edges {
+ if len(edge.Route) == 0 || edge.Src == nil || edge.Dst == nil {
+ continue
+ }
+ srcDelta := ancestorDelta(edge.Src, deltas)
+ dstDelta := ancestorDelta(edge.Dst, deltas)
+ if srcDelta == nil && dstDelta == nil {
+ continue
+ }
+ if srcDelta != nil && dstDelta != nil && sameDelta(srcDelta, dstDelta) {
+ // Both endpoints belong to the same child: preserve the core
+ // layout's route shape by translating it entirely.
+ edge.Move(srcDelta.X, srcDelta.Y)
+ continue
+ }
+ if srcDelta != nil {
+ edge.Route[0].X += srcDelta.X
+ edge.Route[0].Y += srcDelta.Y
+ }
+ if dstDelta != nil {
+ last := edge.Route[len(edge.Route)-1]
+ last.X += dstDelta.X
+ last.Y += dstDelta.Y
+ }
+ }
+}
+
+// ancestorDelta returns the delta of the root child that obj descends from.
+func ancestorDelta(obj *d2graph.Object, deltas map[*d2graph.Object]*geo.Point) *geo.Point {
+ for curr := obj; curr != nil; curr = curr.Parent {
+ if delta, ok := deltas[curr]; ok {
+ return delta
+ }
+ }
+ return nil
+}
+
+func sameDelta(a, b *geo.Point) bool {
+ return a == b || (a.X == b.X && a.Y == b.Y)
+}
+
+// isCycleEdge reports whether both endpoints are direct children of the cycle
+// root, i.e. the edge should follow the circle.
+func isCycleEdge(g *d2graph.Graph, edge *d2graph.Edge) bool {
+ if edge.Src == nil || edge.Dst == nil {
+ return false
+ }
+ return edge.Src.Parent == g.Root && edge.Dst.Parent == g.Root
+}
+
+// arcDirections decides the travel direction of every cycle edge. With only
+// two children on the ring, edges between the same pair in opposite
+// directions take opposite semicircles so their arcs do not overlap. On
+// larger rings the opposite arc would cross other children, so every edge
+// takes the shorter way around.
+func arcDirections(g *d2graph.Graph) map[*d2graph.Edge]float64 {
+ ringSize := len(g.Root.ChildrenArray)
+ type pairKey struct{ a, b string }
+ pairs := make(map[pairKey][]*d2graph.Edge)
+ order := make([]pairKey, 0, len(g.Edges))
+
+ for _, edge := range g.Edges {
+ if !isCycleEdge(g, edge) {
+ continue
+ }
+ a, b := edge.Src.AbsID(), edge.Dst.AbsID()
+ if a > b {
+ a, b = b, a
+ }
+ key := pairKey{a, b}
+ if _, ok := pairs[key]; !ok {
+ order = append(order, key)
+ }
+ pairs[key] = append(pairs[key], edge)
+ }
+
+ directions := make(map[*d2graph.Edge]float64, len(g.Edges))
+ for _, key := range order {
+ edges := pairs[key]
+ opposite := ringSize == 2 && len(edges) > 1 &&
+ edges[0].Src.AbsID() != edges[len(edges)-1].Src.AbsID()
+ for i, edge := range edges {
+ srcAngle := math.Atan2(edge.Src.Center().Y, edge.Src.Center().X)
+ dstAngle := math.Atan2(edge.Dst.Center().Y, edge.Dst.Center().X)
+ cw := modulo2Pi(dstAngle - srcAngle)
+ cclw := modulo2Pi(srcAngle - dstAngle)
+ if edge.Src == edge.Dst {
+ directions[edge] = -1
+ } else if opposite && i > 0 {
+ // Same angular travel direction from the reversed endpoints
+ // traces the complementary arc, so the two edges do not
+ // overlap.
+ directions[edge] = directions[edges[0]]
+ } else if cw <= cclw {
+ directions[edge] = 1
+ } else {
+ directions[edge] = -1
+ }
+ }
+ }
+ return directions
+}
+
+// routeCircularArc replaces the edge's route with an exact circular arc along
+// the ring, trimmed to the visible borders of both endpoints. dir is +1 for
+// clockwise travel, -1 for counter-clockwise.
+func routeCircularArc(edge *d2graph.Edge, radius, dir float64) {
+ if edge.Src == nil || edge.Dst == nil || len(edge.Route) == 0 {
+ return
+ }
+
+ center := geo.NewPoint(0, 0)
+ srcAngle := math.Atan2(edge.Src.Center().Y, edge.Src.Center().X)
+ dstAngle := math.Atan2(edge.Dst.Center().Y, edge.Dst.Center().X)
+
+ exit := borderCrossingFor(edge.Src, center, radius, srcAngle, dir)
+ entry := borderCrossingFor(edge.Dst, center, radius, dstAngle, -dir)
+ if exit == nil || entry == nil {
+ routeStraight(edge)
+ return
+ }
+
+ // Total swept angle from exit to entry travelling in direction dir.
+ sweep := modulo2Pi(entry.angle - exit.angle)
+ if dir < 0 {
+ sweep -= 2 * math.Pi
+ }
+ if math.Abs(sweep) < epsilon {
+ routeStraight(edge)
+ return
+ }
+
+ start := exit.point
+ if start == nil {
+ start = pointOnCircle(center, radius, exit.angle)
+ }
+ end := entry.point
+ if end == nil {
+ end = pointOnCircle(center, radius, entry.angle)
+ }
+
+ edge.Route = buildArcRoute(center, radius, exit.angle, exit.angle+sweep)
+ edge.Route[0] = start
+ edge.Route[len(edge.Route)-1] = end
+ edge.IsCurve = true
+}
+
+func routeStraight(edge *d2graph.Edge) {
+ if edge.Src == nil || edge.Dst == nil {
+ return
+ }
+ edge.Route = []*geo.Point{edge.Src.Center(), edge.Dst.Center()}
+ edge.TraceToShape(edge.Route, 0, 1)
+ edge.IsCurve = false
+}
+
+type borderCrossing struct {
+ angle float64
+ // point is set when the crossing lies on the shape's visible perimeter;
+ // otherwise the point on the circle at angle is used.
+ point *geo.Point
+}
+
+// borderCrossingFor returns where the circle leaves obj's border when
+// travelling from the object's center angle in the given direction (searchDir
+// is the direction to search in: +dir for an exit, -dir for an entry).
+func borderCrossingFor(obj *d2graph.Object, center *geo.Point, radius, objAngle, searchDir float64) *borderCrossing {
+ if p := perimeterCrossing(obj, center, radius, objAngle, searchDir); p != nil {
+ return p
+ }
+
+ // Fall back to the box border: bisect between the object's center angle
+ // (inside) and a quarter turn away (outside), then project onto the
+ // visible border for non-rectangular shapes.
+ inside := objAngle
+ outside := objAngle + searchDir*math.Pi/2
+ for i := 0; i < 48; i++ {
+ mid := (inside + outside) / 2
+ if obj.Box.Contains(pointOnCircle(center, radius, mid)) {
+ inside = mid
+ } else {
+ outside = mid
+ }
+ }
+ if math.Abs(outside-objAngle) < epsilon {
+ return nil
+ }
+ p := pointOnCircle(center, radius, outside)
+ if s := obj.ToShape(); s != nil && !s.IsRectangular() {
+ p = shape.TraceToShapeBorder(s, p, pointOnCircle(center, radius, outside-searchDir*0.001))
+ }
+ return &borderCrossing{angle: math.Atan2(p.Y-center.Y, p.X-center.X), point: p}
+}
+
+// perimeterCrossing intersects the circle with the shape's visible perimeter
+// by marching a quarter turn from the object's center angle with chords. It
+// returns the crossing closest to the center angle: the border exit when
+// marching forward, the border entry when marching backward. nil when the
+// shape has no perimeter geometry or no crossing was found.
+func perimeterCrossing(obj *d2graph.Object, center *geo.Point, radius, objAngle, searchDir float64) *borderCrossing {
+ s := obj.ToShape()
+ if s == nil || s.IsRectangular() {
+ return nil
+ }
+ perimeter := s.Perimeter()
+ if len(perimeter) == 0 {
+ return nil
+ }
+
+ from := objAngle
+ to := objAngle + searchDir*math.Pi/2
+ steps := int(math.Ceil(math.Abs(to-from) / chordStep))
+ if steps < 8 {
+ steps = 8
+ }
+
+ var best *borderCrossing
+ bestDist := math.Inf(1)
+ for i := 0; i < steps; i++ {
+ a0 := from + (to-from)*float64(i)/float64(steps)
+ a1 := from + (to-from)*float64(i+1)/float64(steps)
+ chord := geo.Segment{
+ Start: pointOnCircle(center, radius, a0),
+ End: pointOnCircle(center, radius, a1),
+ }
+ for _, elem := range perimeter {
+ for _, p := range elem.Intersections(chord) {
+ angle := math.Atan2(p.Y-center.Y, p.X-center.X)
+ dist := math.Abs(moduloSigned(angle - objAngle))
+ if dist < bestDist {
+ bestDist = dist
+ best = &borderCrossing{angle: angle, point: p}
+ }
+ }
+ }
+ }
+ return best
+}
+
+// buildArcRoute returns a route of cubic Bezier control points tracing the
+// circle from startAngle to endAngle, split into segments of at most
+// maxArcSweep. Every chain endpoint lies exactly on the circle.
+func buildArcRoute(center *geo.Point, radius, startAngle, endAngle float64) []*geo.Point {
+ sweep := endAngle - startAngle
+ segments := int(math.Ceil(math.Abs(sweep) / maxArcSweep))
+ if segments < 1 {
+ segments = 1
+ }
+
+ route := make([]*geo.Point, 1, 3*segments+1)
+ route[0] = pointOnCircle(center, radius, startAngle)
+ for i := 0; i < segments; i++ {
+ a0 := startAngle + sweep*float64(i)/float64(segments)
+ a1 := startAngle + sweep*float64(i+1)/float64(segments)
+ p0 := pointOnCircle(center, radius, a0)
+ p3 := pointOnCircle(center, radius, a1)
+ // Canonical circular arc to cubic Bezier control point offset.
+ k := 4. / 3. * math.Tan((a1-a0)/4.)
+
+ c1 := geo.NewPoint(
+ p0.X-k*radius*math.Sin(a0),
+ p0.Y+k*radius*math.Cos(a0),
+ )
+ c2 := geo.NewPoint(
+ p3.X+k*radius*math.Sin(a1),
+ p3.Y-k*radius*math.Cos(a1),
+ )
+
+ route = append(route, c1, c2, p3)
+ }
+ return route
+}
+
+func pointOnCircle(center *geo.Point, radius, angle float64) *geo.Point {
+ return geo.NewPoint(center.X+radius*math.Cos(angle), center.Y+radius*math.Sin(angle))
+}
+
+// normalizeGraph shifts all objects and routes so the bounding box starts at
+// (0, 0) and records the padded bounding box on the root.
+func normalizeGraph(g *d2graph.Graph) {
+ tl := geo.NewPoint(math.Inf(1), math.Inf(1))
+ br := geo.NewPoint(math.Inf(-1), math.Inf(-1))
+
+ for _, obj := range g.Objects {
+ if obj.TopLeft == nil {
+ continue
+ }
+ tl.X = math.Min(tl.X, obj.TopLeft.X)
+ tl.Y = math.Min(tl.Y, obj.TopLeft.Y)
+ br.X = math.Max(br.X, obj.TopLeft.X+obj.Width)
+ br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height)
+ }
+ for _, edge := range g.Edges {
+ for _, point := range edge.Route {
+ tl.X = math.Min(tl.X, point.X)
+ tl.Y = math.Min(tl.Y, point.Y)
+ br.X = math.Max(br.X, point.X)
+ br.Y = math.Max(br.Y, point.Y)
+ }
+ }
+
+ if math.IsInf(tl.X, 0) || math.IsInf(tl.Y, 0) {
+ return
+ }
+
+ contentPadding := containerContentPadding(g.Root)
+ dx := contentPadding.Left - tl.X
+ dy := contentPadding.Top - tl.Y
+ if dx != 0 || dy != 0 {
+ for _, obj := range g.Objects {
+ if obj.TopLeft != nil {
+ obj.TopLeft.X += dx
+ obj.TopLeft.Y += dy
+ }
+ }
+ for _, edge := range g.Edges {
+ edge.Move(dx, dy)
+ }
+ }
+ g.Root.Box = geo.NewBox(
+ geo.NewPoint(0, 0),
+ br.X-tl.X+contentPadding.Left+contentPadding.Right,
+ br.Y-tl.Y+contentPadding.Top+contentPadding.Bottom,
+ )
+}
+
+func containerContentPadding(root *d2graph.Object) geo.Spacing {
+ padding := geo.Spacing{
+ Top: containerPadding,
+ Bottom: containerPadding,
+ Left: containerPadding,
+ Right: containerPadding,
+ }
+ if root == nil {
+ return padding
+ }
+ margin, spacing := root.Spacing()
+ padding.Top = math.Max(padding.Top, math.Max(margin.Top, spacing.Top))
+ padding.Bottom = math.Max(padding.Bottom, math.Max(margin.Bottom, spacing.Bottom))
+ padding.Left = math.Max(padding.Left, math.Max(margin.Left, spacing.Left))
+ padding.Right = math.Max(padding.Right, math.Max(margin.Right, spacing.Right))
+ return padding
+}
+
+func modulo2Pi(angle float64) float64 {
+ angle = math.Mod(angle, 2*math.Pi)
+ if angle < 0 {
+ angle += 2 * math.Pi
+ }
+ return angle
+}
+
+// moduloSigned reduces an angle to [0, 2Ï€) keeping the magnitude of signed
+// offsets small enough for nearest-crossing comparisons.
+func moduloSigned(angle float64) float64 {
+ angle = math.Mod(angle, 2*math.Pi)
+ if angle < 0 {
+ angle += 2 * math.Pi
+ }
+ return angle
+}
+
+// positionLabelsIcons matches the default label and icon placement used by
+// the core layouts.
+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..9125c0e637
--- /dev/null
+++ b/d2layouts/d2cycle/layout_test.go
@@ -0,0 +1,319 @@
+package d2cycle
+
+import (
+ "context"
+ "math"
+ "strings"
+ "testing"
+
+ "github.com/d2lang/d2/d2compiler"
+ "github.com/d2lang/d2/d2graph"
+ "github.com/d2lang/d2/d2target"
+ "github.com/d2lang/d2/lib/geo"
+ "github.com/d2lang/d2/lib/label"
+)
+
+func compileGraph(t *testing.T, script string) *d2graph.Graph {
+ t.Helper()
+ g, _, err := d2compiler.Compile("", strings.NewReader(script), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return g
+}
+
+// fakeCoreLayout gives every object a 100x100 box, spaces root children
+// horizontally and routes all edges as straight lines between centers.
+func fakeCoreLayout(_ context.Context, g *d2graph.Graph) error {
+ x := 0.
+ for _, obj := range g.Root.ChildrenArray {
+ setBox(obj, geo.NewPoint(x, 0), 100, 100)
+ x += 300
+ }
+ for _, obj := range g.Objects {
+ if obj.Parent == nil || obj.Parent == g.Root {
+ continue
+ }
+ p := obj.Parent
+ setBox(obj, geo.NewPoint(p.TopLeft.X+20, p.TopLeft.Y+20), 30, 30)
+ }
+ for _, edge := range g.Edges {
+ edge.Route = []*geo.Point{edge.Src.Center(), edge.Dst.Center()}
+ }
+ return nil
+}
+
+func setBox(obj *d2graph.Object, tl *geo.Point, w, h float64) {
+ obj.Box = geo.NewBox(tl, w, h)
+}
+
+func TestCalculateRadius(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+a
+`)
+ if got := calculateRadius(g.Root.ChildrenArray); got != 0 {
+ t.Fatalf("expected radius 0 for single child, got %v", got)
+ }
+
+ g = compileGraph(t, `shape: cycle
+a; b; c
+`)
+ for _, obj := range g.Objects {
+ setBox(obj, geo.NewPoint(0, 0), 300, 300)
+ }
+ radius := calculateRadius(g.Root.ChildrenArray)
+ // Children must fit: half diagonal + padding away from each other along
+ // the chord between adjacent positions.
+ want := (150*math.Sqrt2 + padding) / math.Sin(math.Pi/3)
+ if math.Abs(radius-want) > 0.001 {
+ t.Fatalf("expected radius %v, got %v", want, radius)
+ }
+
+ g = compileGraph(t, `shape: cycle
+a; b
+`)
+ for _, obj := range g.Objects {
+ setBox(obj, geo.NewPoint(0, 0), 40, 40)
+ }
+ if got := calculateRadius(g.Root.ChildrenArray); got != minRadius {
+ t.Fatalf("expected small children to use the minimum radius %v, got %v", minRadius, got)
+ }
+}
+
+func TestLayoutPositionsChildrenOnCircle(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+a -> b -> c -> d -> a
+`)
+ if err := Layout(context.Background(), g, fakeCoreLayout); err != nil {
+ t.Fatal(err)
+ }
+
+ children := g.Root.ChildrenArray
+ if len(children) != 4 {
+ t.Fatalf("expected 4 children, got %d", len(children))
+ }
+ var cx, cy float64
+ for _, obj := range children {
+ cx += obj.Center().X
+ cy += obj.Center().Y
+ }
+ cx /= float64(len(children))
+ cy /= float64(len(children))
+
+ radius := -1.
+ for _, obj := range children {
+ d := math.Hypot(obj.Center().X-cx, obj.Center().Y-cy)
+ if radius == -1 {
+ radius = d
+ }
+ if math.Abs(d-radius) > 0.001 {
+ t.Fatalf("expected %q at radius %v from (%v,%v), got %v", obj.ID, radius, cx, cy, d)
+ }
+ }
+ if radius < minRadius-0.001 {
+ t.Fatalf("expected radius >= %v, got %v", minRadius, radius)
+ }
+
+ // First child sits at 12 o'clock, the rest clockwise.
+ first := children[0]
+ if math.Abs(first.Center().X-cx) > 0.001 || first.Center().Y >= cy {
+ t.Fatalf("expected first child centered on top, got (%v,%v) with center (%v,%v)", first.Center().X, first.Center().Y, cx, cy)
+ }
+ second := children[1]
+ if second.Center().X <= cx {
+ t.Fatalf("expected second child on the right side, got (%v,%v)", second.Center().X, second.Center().Y)
+ }
+}
+
+func TestLayoutAddsContainerInset(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+a -> b -> c -> d -> a
+`)
+ g.Root.Label.Value = "Cycle"
+ g.Root.LabelDimensions = d2target.TextDimensions{Width: 80, Height: 40}
+ labelPosition := label.InsideTopCenter.String()
+ g.Root.LabelPosition = &labelPosition
+ if err := Layout(context.Background(), g, fakeCoreLayout); err != nil {
+ t.Fatal(err)
+ }
+
+ root := g.Root.Box
+ if root == nil {
+ t.Fatal("expected cycle root box")
+ }
+ for _, child := range g.Root.ChildrenArray {
+ if child.TopLeft.X <= root.TopLeft.X || child.TopLeft.Y < root.TopLeft.Y+50 ||
+ child.TopLeft.X+child.Width >= root.TopLeft.X+root.Width ||
+ child.TopLeft.Y+child.Height >= root.TopLeft.Y+root.Height {
+ t.Fatalf("expected child %q to be inset from cycle container, got child %v and root %v", child.ID, child.TopLeft, root.TopLeft)
+ }
+ }
+ for _, edge := range g.Edges {
+ if !edge.IsCurve {
+ t.Fatalf("expected cycle edge %q to remain curved", edge.AbsID())
+ }
+ for _, point := range edge.Route {
+ if point.X <= root.TopLeft.X || point.Y <= root.TopLeft.Y ||
+ point.X >= root.TopLeft.X+root.Width || point.Y >= root.TopLeft.Y+root.Height {
+ t.Fatalf("expected edge %q point %v to be inset from cycle container %v", edge.AbsID(), point, root.TopLeft)
+ }
+ }
+ }
+}
+
+func TestLayoutRoutesCircularArcsTrimmedToBorders(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+a -> b -> c -> d -> a
+`)
+ if err := Layout(context.Background(), g, fakeCoreLayout); err != nil {
+ t.Fatal(err)
+ }
+
+ byID := make(map[string]*d2graph.Object)
+ for _, obj := range g.Objects {
+ byID[obj.ID] = obj
+ }
+ var cx, cy, radius float64
+ for i, obj := range g.Root.ChildrenArray {
+ cx += obj.Center().X
+ cy += obj.Center().Y
+ if i == len(g.Root.ChildrenArray)-1 {
+ cx /= float64(len(g.Root.ChildrenArray))
+ cy /= float64(len(g.Root.ChildrenArray))
+ radius = math.Hypot(byID["a"].Center().X-cx, byID["a"].Center().Y-cy)
+ }
+ }
+
+ for _, edge := range g.Edges {
+ if !edge.IsCurve {
+ t.Fatalf("expected edge %q to be routed as a curve", edge.AbsID())
+ }
+ route := edge.Route
+ // Bezier chain: 1 + 3n points.
+ if len(route) < 4 || (len(route)-1)%3 != 0 {
+ t.Fatalf("expected edge %q route to be a Bezier chain, got %d points", edge.AbsID(), len(route))
+ }
+ for i, p := range route {
+ if i%3 != 0 {
+ continue
+ }
+ if d := math.Abs(math.Hypot(p.X-cx, p.Y-cy) - radius); d > 0.001 {
+ t.Fatalf("expected chain endpoint %v of edge %q on the circle (radius %v), off by %v", p, edge.AbsID(), radius, d)
+ }
+ }
+ // Endpoints must stop at the endpoints' borders, not their centers.
+ if borderGap(route[0], byID[edge.Src.ID]) > 0.001 {
+ t.Fatalf("expected edge %q to start on the source border, got %v", edge.AbsID(), route[0])
+ }
+ if borderGap(route[len(route)-1], byID[edge.Dst.ID]) > 0.001 {
+ t.Fatalf("expected edge %q to end on the destination border, got %v", edge.AbsID(), route[len(route)-1])
+ }
+ }
+}
+
+func borderGap(p *geo.Point, obj *d2graph.Object) float64 {
+ dx := math.Max(math.Abs(p.X-obj.Center().X)-obj.Width/2, 0)
+ dy := math.Max(math.Abs(p.Y-obj.Center().Y)-obj.Height/2, 0)
+ return math.Hypot(dx, dy)
+}
+
+func TestLayoutKeepsNestedContentAndCrossChildEdges(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+x: {
+ p -> q
+}
+y
+x.p -> y
+`)
+ if err := Layout(context.Background(), g, fakeCoreLayout); err != nil {
+ t.Fatal(err)
+ }
+
+ var p, q *d2graph.Object
+ for _, obj := range g.Objects {
+ switch obj.AbsID() {
+ case "x.p":
+ p = obj
+ case "x.q":
+ q = obj
+ }
+ }
+ if p == nil || q == nil {
+ t.Fatal("expected nested objects to survive the cycle layout")
+ }
+ // p and q keep their internal offset inside x after x moved onto the ring.
+ x := g.Root.ChildrenArray[0]
+ if math.Abs(p.Center().X-(x.TopLeft.X+20+15)) > 0.001 || math.Abs(p.Center().Y-(x.TopLeft.Y+20+15)) > 0.001 {
+ t.Fatalf("expected nested content to move with its container, got p at (%v,%v) with x at %v", p.Center().X, p.Center().Y, x.TopLeft)
+ }
+
+ for _, edge := range g.Edges {
+ srcInX, dstInX := edge.Src.Parent == x, edge.Dst.Parent == x
+ if srcInX && dstInX {
+ if !samePoint(edge.Route[0], edge.Src.Center()) || !samePoint(edge.Route[len(edge.Route)-1], edge.Dst.Center()) {
+ t.Fatalf("expected internal edge of %q to move with the container", x.ID)
+ }
+ }
+ if !srcInX && dstInX || srcInX && !dstInX {
+ if !samePoint(edge.Route[0], edge.Src.Center()) || !samePoint(edge.Route[len(edge.Route)-1], edge.Dst.Center()) {
+ t.Fatalf("expected cross-child edge to stay attached to its endpoints")
+ }
+ }
+ }
+}
+
+func TestLayoutOppositeEdgesOnTwoNodeCycleDoNotOverlap(t *testing.T) {
+ g := compileGraph(t, `shape: cycle
+a -> b
+b -> a
+`)
+ if err := Layout(context.Background(), g, fakeCoreLayout); err != nil {
+ t.Fatal(err)
+ }
+
+ var cx, cy float64
+ for _, obj := range g.Root.ChildrenArray {
+ cx += obj.Center().X
+ cy += obj.Center().Y
+ }
+ cx /= 2
+ cy /= 2
+
+ angles := make([][]float64, 0, 2)
+ for _, edge := range g.Edges {
+ var as, ae float64
+ for i, p := range edge.Route {
+ if i%3 != 0 {
+ continue
+ }
+ a := math.Atan2(p.Y-cy, p.X-cx)
+ switch i {
+ case 0:
+ as = a
+ default:
+ ae = a
+ }
+ }
+ angles = append(angles, []float64{modulo2Pi(as), modulo2Pi(ae)})
+ }
+
+ // One arc must pass through the right side (angle 0), the other through
+ // the left side (angle pi).
+ passesZero, passesPi := false, false
+ for _, a := range angles {
+ mid := modulo2Pi(a[0] + modulo2Pi(a[1]-a[0])/2)
+ if math.Abs(mid) < math.Pi/4 || math.Abs(mid) > 7*math.Pi/4 {
+ passesZero = true
+ }
+ if math.Abs(mid-math.Pi) < math.Pi/4 {
+ passesPi = true
+ }
+ }
+ if !passesZero || !passesPi {
+ t.Fatalf("expected opposite edges to take opposite semicircles, got %v", angles)
+ }
+}
+
+func samePoint(a, b *geo.Point) bool {
+ return math.Abs(a.X-b.X) < 0.001 && math.Abs(a.Y-b.Y) < 0.001
+}
diff --git a/d2layouts/d2layouts.go b/d2layouts/d2layouts.go
index c9c45479df..531a4b842e 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"
@@ -26,6 +27,7 @@ const (
ConstantNearGraph DiagramType = "constant-near"
GridDiagram DiagramType = "grid-diagram"
SequenceDiagram DiagramType = "sequence-diagram"
+ CycleDiagram DiagramType = "cycle-diagram"
)
type GraphInfo struct {
@@ -289,6 +291,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 +401,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/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/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 2d5fbfc156..f49b42486f 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -1829,7 +1829,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 432745bece..0d274a469c 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -1266,6 +1266,7 @@ const (
ShapeSQLTable = "sql_table"
ShapeImage = "image"
ShapeSequenceDiagram = "sequence_diagram"
+ ShapeCycleDiagram = "cycle"
ShapeHierarchy = "hierarchy"
)
@@ -1294,6 +1295,7 @@ var Shapes = []string{
ShapeSQLTable,
ShapeImage,
ShapeSequenceDiagram,
+ ShapeCycleDiagram,
ShapeHierarchy,
}
@@ -1364,6 +1366,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..5648bcd0dc
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
@@ -0,0 +1,1567 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "four",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 62
+ },
+ "width": 513,
+ "height": 526,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "four.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 230,
+ "y": 92
+ },
+ "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": "four.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 430,
+ "y": 292
+ },
+ "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": "four.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 230,
+ "y": 492
+ },
+ "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": "four.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 30,
+ "y": 292
+ },
+ "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": "nonrect",
+ "type": "cycle",
+ "pos": {
+ "x": 573,
+ "y": 76
+ },
+ "width": 475,
+ "height": 499,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "nonrect.a",
+ "type": "oval",
+ "pos": {
+ "x": 772,
+ "y": 106
+ },
+ "width": 77,
+ "height": 77,
+ "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": "nonrect.b",
+ "type": "hexagon",
+ "pos": {
+ "x": 958,
+ "y": 410
+ },
+ "width": 50,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "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": "nonrect.c",
+ "type": "diamond",
+ "pos": {
+ "x": 614,
+ "y": 398
+ },
+ "width": 46,
+ "height": 92,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N4",
+ "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": "mixed",
+ "type": "cycle",
+ "pos": {
+ "x": 1108,
+ "y": 0
+ },
+ "width": 528,
+ "height": 650,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "red",
+ "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": "mixed.x",
+ "type": "rectangle",
+ "pos": {
+ "x": 1303,
+ "y": 30
+ },
+ "width": 167,
+ "height": 292,
+ "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": "x",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 11,
+ "labelHeight": 31,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "mixed.x.p",
+ "type": "rectangle",
+ "pos": {
+ "x": 1387,
+ "y": 60
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "p",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.x.q",
+ "type": "rectangle",
+ "pos": {
+ "x": 1333,
+ "y": 226
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "q",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.y",
+ "type": "rectangle",
+ "pos": {
+ "x": 1552,
+ "y": 475
+ },
+ "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": "y",
+ "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": "mixed.z",
+ "type": "oval",
+ "pos": {
+ "x": 1156,
+ "y": 470
+ },
+ "width": 77,
+ "height": 77,
+ "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": "z",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 7,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "two",
+ "type": "cycle",
+ "pos": {
+ "x": 1696,
+ "y": 62
+ },
+ "width": 460,
+ "height": 526,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "two.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 1899,
+ "y": 92
+ },
+ "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": "two.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 1899,
+ "y": 492
+ },
+ "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": "four.(a -> b)[0]",
+ "src": "four.a",
+ "srcArrow": "none",
+ "dst": "four.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": 283.5,
+ "y": 126.76300048828125
+ },
+ {
+ "x": 370.5150146484375,
+ "y": 138.39500427246094
+ },
+ {
+ "x": 439.77301025390625,
+ "y": 205.41299438476562
+ },
+ {
+ "x": 454.25799560546875,
+ "y": 291.9989929199219
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(b -> c)[0]",
+ "src": "four.b",
+ "srcArrow": "none",
+ "dst": "four.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": 454.25799560546875,
+ "y": 358
+ },
+ {
+ "x": 439.77301025390625,
+ "y": 444.58599853515625
+ },
+ {
+ "x": 370.5150146484375,
+ "y": 511.60400390625
+ },
+ {
+ "x": 283.5,
+ "y": 523.2360229492188
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(c -> d)[0]",
+ "src": "four.c",
+ "srcArrow": "none",
+ "dst": "four.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": 230.49899291992188,
+ "y": 523.2360229492188
+ },
+ {
+ "x": 143.48399353027344,
+ "y": 511.60400390625
+ },
+ {
+ "x": 74.22599792480469,
+ "y": 444.58599853515625
+ },
+ {
+ "x": 59.74100112915039,
+ "y": 358
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(d -> a)[0]",
+ "src": "four.d",
+ "srcArrow": "none",
+ "dst": "four.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": 59.74100112915039,
+ "y": 291.9989929199219
+ },
+ {
+ "x": 74.22599792480469,
+ "y": 205.41299438476562
+ },
+ {
+ "x": 143.48399353027344,
+ "y": 138.39500427246094
+ },
+ {
+ "x": 230.49899291992188,
+ "y": 126.76300048828125
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(a -> b)[0]",
+ "src": "nonrect.a",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 849.0819702148438,
+ "y": 148.20599365234375
+ },
+ {
+ "x": 906.791015625,
+ "y": 159.4709930419922
+ },
+ {
+ "x": 956.6190185546875,
+ "y": 195.56100463867188
+ },
+ {
+ "x": 985.3189697265625,
+ "y": 246.8800048828125
+ },
+ {
+ "x": 1014.0180053710938,
+ "y": 298.1990051269531
+ },
+ {
+ "x": 1018.6810302734375,
+ "y": 359.5469970703125
+ },
+ {
+ "x": 997.760986328125,
+ "y": 414.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> c)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 967.760986328125,
+ "y": 468.5
+ },
+ {
+ "x": 929.239013671875,
+ "y": 517.1719970703125
+ },
+ {
+ "x": 870.3289794921875,
+ "y": 545.260009765625
+ },
+ {
+ "x": 808.260986328125,
+ "y": 544.4840087890625
+ },
+ {
+ "x": 746.1939697265625,
+ "y": 543.7080078125
+ },
+ {
+ "x": 688.0040283203125,
+ "y": 514.1580200195312
+ },
+ {
+ "x": 650.760986328125,
+ "y": 464.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(c -> a)[0]",
+ "src": "nonrect.c",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 625.760986328125,
+ "y": 420.5
+ },
+ {
+ "x": 603,
+ "y": 365.0880126953125
+ },
+ {
+ "x": 606.27197265625,
+ "y": 302.3909912109375
+ },
+ {
+ "x": 634.6820068359375,
+ "y": 249.6510009765625
+ },
+ {
+ "x": 663.0910034179688,
+ "y": 196.91099548339844
+ },
+ {
+ "x": 713.64501953125,
+ "y": 159.68299865722656
+ },
+ {
+ "x": 772.4390258789062,
+ "y": 148.20599365234375
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> a)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 997.760986328125,
+ "y": 414.5
+ },
+ {
+ "x": 1018.6810302734375,
+ "y": 359.5469970703125
+ },
+ {
+ "x": 1014.0180053710938,
+ "y": 298.1990051269531
+ },
+ {
+ "x": 985.3189697265625,
+ "y": 246.8800048828125
+ },
+ {
+ "x": 956.6190185546875,
+ "y": 195.56100463867188
+ },
+ {
+ "x": 906.791015625,
+ "y": 159.4709930419922
+ },
+ {
+ "x": 849.0819702148438,
+ "y": 148.20599365234375
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.x.(p -> q)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.x.q",
+ "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": 1392.9300537109375,
+ "y": 126
+ },
+ {
+ "x": 1367.3299560546875,
+ "y": 166
+ },
+ {
+ "x": 1360.9300537109375,
+ "y": 186
+ },
+ {
+ "x": 1360.9300537109375,
+ "y": 226
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(x.p -> y)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.y",
+ "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": 1427.9300537109375,
+ "y": 126
+ },
+ {
+ "x": 1537.4300537109375,
+ "y": 553.9229736328125
+ },
+ {
+ "x": 1556.030029296875,
+ "y": 575.9229736328125
+ },
+ {
+ "x": 1552.6209716796875,
+ "y": 485.8840026855469
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(y -> z)[0]",
+ "src": "mixed.y",
+ "srcArrow": "none",
+ "dst": "mixed.z",
+ "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": 1556.322998046875,
+ "y": 541.8839721679688
+ },
+ {
+ "x": 1513.9429931640625,
+ "y": 591.60400390625
+ },
+ {
+ "x": 1451.8089599609375,
+ "y": 620.1240234375
+ },
+ {
+ "x": 1386.47900390625,
+ "y": 619.843994140625
+ },
+ {
+ "x": 1321.14794921875,
+ "y": 619.5640258789062
+ },
+ {
+ "x": 1259.260986328125,
+ "y": 590.5120239257812
+ },
+ {
+ "x": 1217.31005859375,
+ "y": 540.4299926757812
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(z -> x)[0]",
+ "src": "mixed.z",
+ "srcArrow": "none",
+ "dst": "mixed.x",
+ "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": 1178.9549560546875,
+ "y": 473.99798583984375
+ },
+ {
+ "x": 1138,
+ "y": 361.76800537109375
+ },
+ {
+ "x": 1193.239990234375,
+ "y": 237.25799560546875
+ },
+ {
+ "x": 1303.9300537109375,
+ "y": 192.3070068359375
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(a -> b)[0]",
+ "src": "two.a",
+ "srcArrow": "none",
+ "dst": "two.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": 1952.5,
+ "y": 126.76300048828125
+ },
+ {
+ "x": 2051.8310546875,
+ "y": 140.04100036621094
+ },
+ {
+ "x": 2126,
+ "y": 224.78500366210938
+ },
+ {
+ "x": 2126,
+ "y": 325
+ },
+ {
+ "x": 2126,
+ "y": 425.2139892578125
+ },
+ {
+ "x": 2051.8310546875,
+ "y": 509.9580078125
+ },
+ {
+ "x": 1952.5,
+ "y": 523.2360229492188
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(b -> a)[0]",
+ "src": "two.b",
+ "srcArrow": "none",
+ "dst": "two.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": 1899.4990234375,
+ "y": 523.2360229492188
+ },
+ {
+ "x": 1800.16796875,
+ "y": 509.9580078125
+ },
+ {
+ "x": 1726,
+ "y": 425.2139892578125
+ },
+ {
+ "x": 1726,
+ "y": 325
+ },
+ {
+ "x": 1726,
+ "y": 224.78500366210938
+ },
+ {
+ "x": 1800.16796875,
+ "y": 140.04100036621094
+ },
+ {
+ "x": 1899.4990234375,
+ "y": 126.76300048828125
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
new file mode 100644
index 0000000000..83da95a92f
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
@@ -0,0 +1,40 @@
+
\ 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..bccfe47cc3
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
@@ -0,0 +1,1565 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "four",
+ "type": "cycle",
+ "pos": {
+ "x": 12,
+ "y": 99
+ },
+ "width": 514,
+ "height": 526,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "four.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 242,
+ "y": 129
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "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": "four.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 442,
+ "y": 329
+ },
+ "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": "four.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 242,
+ "y": 529
+ },
+ "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": "four.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 42,
+ "y": 329
+ },
+ "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": "nonrect",
+ "type": "cycle",
+ "pos": {
+ "x": 545,
+ "y": 111
+ },
+ "width": 481,
+ "height": 501,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "nonrect.a",
+ "type": "oval",
+ "pos": {
+ "x": 743,
+ "y": 141
+ },
+ "width": 80,
+ "height": 77,
+ "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": "nonrect.b",
+ "type": "hexagon",
+ "pos": {
+ "x": 916,
+ "y": 445
+ },
+ "width": 80,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "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": "nonrect.c",
+ "type": "diamond",
+ "pos": {
+ "x": 586,
+ "y": 434
+ },
+ "width": 46,
+ "height": 92,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N4",
+ "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": "mixed",
+ "type": "cycle",
+ "pos": {
+ "x": 1046,
+ "y": 12
+ },
+ "width": 569,
+ "height": 701,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "red",
+ "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": "mixed.x",
+ "type": "rectangle",
+ "pos": {
+ "x": 1247,
+ "y": 42
+ },
+ "width": 201,
+ "height": 312,
+ "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": "x",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 11,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "mixed.x.p",
+ "type": "rectangle",
+ "pos": {
+ "x": 1317,
+ "y": 92
+ },
+ "width": 80,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "p",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.x.q",
+ "type": "rectangle",
+ "pos": {
+ "x": 1297,
+ "y": 238
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "q",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.y",
+ "type": "rectangle",
+ "pos": {
+ "x": 1530,
+ "y": 527
+ },
+ "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": "y",
+ "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": "mixed.z",
+ "type": "oval",
+ "pos": {
+ "x": 1099,
+ "y": 522
+ },
+ "width": 77,
+ "height": 77,
+ "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": "z",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 7,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "two",
+ "type": "cycle",
+ "pos": {
+ "x": 1634,
+ "y": 99
+ },
+ "width": 460,
+ "height": 526,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "two.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 1838,
+ "y": 129
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "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": "two.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 1838,
+ "y": 529
+ },
+ "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": "four.(a -> b)[0]",
+ "src": "four.a",
+ "srcArrow": "none",
+ "dst": "four.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": 295.5,
+ "y": 163.8780059814453
+ },
+ {
+ "x": 382.5150146484375,
+ "y": 175.50999450683594
+ },
+ {
+ "x": 451.77301025390625,
+ "y": 242.5279998779297
+ },
+ {
+ "x": 466.25799560546875,
+ "y": 329.114990234375
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(b -> c)[0]",
+ "src": "four.b",
+ "srcArrow": "none",
+ "dst": "four.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": 466.25799560546875,
+ "y": 395.114990234375
+ },
+ {
+ "x": 451.77301025390625,
+ "y": 481.70098876953125
+ },
+ {
+ "x": 382.5150146484375,
+ "y": 548.718994140625
+ },
+ {
+ "x": 295.5,
+ "y": 560.3510131835938
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(c -> d)[0]",
+ "src": "four.c",
+ "srcArrow": "none",
+ "dst": "four.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": 242.49899291992188,
+ "y": 560.3510131835938
+ },
+ {
+ "x": 155.48399353027344,
+ "y": 548.718994140625
+ },
+ {
+ "x": 86.22599792480469,
+ "y": 481.70098876953125
+ },
+ {
+ "x": 71.74099731445312,
+ "y": 395.114990234375
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(d -> a)[0]",
+ "src": "four.d",
+ "srcArrow": "none",
+ "dst": "four.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": 71.74099731445312,
+ "y": 329.114990234375
+ },
+ {
+ "x": 86.22599792480469,
+ "y": 242.5279998779297
+ },
+ {
+ "x": 155.48399353027344,
+ "y": 175.50999450683594
+ },
+ {
+ "x": 242.49899291992188,
+ "y": 163.8780059814453
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(a -> b)[0]",
+ "src": "nonrect.a",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 822.9439697265625,
+ "y": 184.35800170898438
+ },
+ {
+ "x": 879.5540161132812,
+ "y": 195.84100341796875
+ },
+ {
+ "x": 928.4299926757812,
+ "y": 231.24400329589844
+ },
+ {
+ "x": 956.9940185546875,
+ "y": 281.4490051269531
+ },
+ {
+ "x": 985.5590209960938,
+ "y": 331.65399169921875
+ },
+ {
+ "x": 991.0230102539062,
+ "y": 391.7569885253906
+ },
+ {
+ "x": 972.1610107421875,
+ "y": 446.35400390625
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> c)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 934.1610107421875,
+ "y": 511.35400390625
+ },
+ {
+ "x": 894.6160278320312,
+ "y": 557.0809936523438
+ },
+ {
+ "x": 836.469970703125,
+ "y": 582.3759765625
+ },
+ {
+ "x": 776.0540161132812,
+ "y": 580.2269897460938
+ },
+ {
+ "x": 715.6370239257812,
+ "y": 578.0789794921875
+ },
+ {
+ "x": 659.4340209960938,
+ "y": 548.718017578125
+ },
+ {
+ "x": 623.1610107421875,
+ "y": 500.35400390625
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(c -> a)[0]",
+ "src": "nonrect.c",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 598.1610107421875,
+ "y": 456.35400390625
+ },
+ {
+ "x": 575.5,
+ "y": 401.1830139160156
+ },
+ {
+ "x": 578.6409912109375,
+ "y": 338.77801513671875
+ },
+ {
+ "x": 606.72998046875,
+ "y": 286.1629943847656
+ },
+ {
+ "x": 634.8189697265625,
+ "y": 233.54800415039062
+ },
+ {
+ "x": 684.9249877929688,
+ "y": 196.21499633789062
+ },
+ {
+ "x": 743.3779907226562,
+ "y": 184.35800170898438
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> a)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 972.1610107421875,
+ "y": 446.35400390625
+ },
+ {
+ "x": 991.0230102539062,
+ "y": 391.7569885253906
+ },
+ {
+ "x": 985.5590209960938,
+ "y": 331.65399169921875
+ },
+ {
+ "x": 956.9940185546875,
+ "y": 281.4490051269531
+ },
+ {
+ "x": 928.4299926757812,
+ "y": 231.24400329589844
+ },
+ {
+ "x": 879.5540161132812,
+ "y": 195.84100341796875
+ },
+ {
+ "x": 822.9439697265625,
+ "y": 184.35800170898438
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.x.(p -> q)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.x.q",
+ "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": 1344.623046875,
+ "y": 158
+ },
+ {
+ "x": 1344.623046875,
+ "y": 198
+ },
+ {
+ "x": 1324.4560546875,
+ "y": 198
+ },
+ {
+ "x": 1324.4560546875,
+ "y": 238
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(x.p -> y)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.y",
+ "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": 1371.2889404296875,
+ "y": 158
+ },
+ {
+ "x": 1483.7889404296875,
+ "y": 647.989990234375
+ },
+ {
+ "x": 1503.9560546875,
+ "y": 647.989990234375
+ },
+ {
+ "x": 1557.5260009765625,
+ "y": 527.9860229492188
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(y -> z)[0]",
+ "src": "mixed.y",
+ "srcArrow": "none",
+ "dst": "mixed.z",
+ "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": 1534.6240234375,
+ "y": 593.9860229492188
+ },
+ {
+ "x": 1488.468017578125,
+ "y": 649.9349975585938
+ },
+ {
+ "x": 1419.6490478515625,
+ "y": 682.22998046875
+ },
+ {
+ "x": 1347.1190185546875,
+ "y": 681.97900390625
+ },
+ {
+ "x": 1274.5889892578125,
+ "y": 681.72900390625
+ },
+ {
+ "x": 1205.9959716796875,
+ "y": 648.9580078125
+ },
+ {
+ "x": 1160.23095703125,
+ "y": 592.68798828125
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(z -> x)[0]",
+ "src": "mixed.z",
+ "srcArrow": "none",
+ "dst": "mixed.x",
+ "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": 1121.85302734375,
+ "y": 526.2160034179688
+ },
+ {
+ "x": 1076.365966796875,
+ "y": 406.9519958496094
+ },
+ {
+ "x": 1131.3409423828125,
+ "y": 272.86700439453125
+ },
+ {
+ "x": 1247.4560546875,
+ "y": 219.8560028076172
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(a -> b)[0]",
+ "src": "two.a",
+ "srcArrow": "none",
+ "dst": "two.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": 1891.0260009765625,
+ "y": 163.8780059814453
+ },
+ {
+ "x": 1990.3570556640625,
+ "y": 177.15699768066406
+ },
+ {
+ "x": 2064.52587890625,
+ "y": 261.8999938964844
+ },
+ {
+ "x": 2064.52587890625,
+ "y": 362.114990234375
+ },
+ {
+ "x": 2064.52587890625,
+ "y": 462.3299865722656
+ },
+ {
+ "x": 1990.3570556640625,
+ "y": 547.072998046875
+ },
+ {
+ "x": 1891.0260009765625,
+ "y": 560.3510131835938
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(b -> a)[0]",
+ "src": "two.b",
+ "srcArrow": "none",
+ "dst": "two.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": 1838.0260009765625,
+ "y": 560.3510131835938
+ },
+ {
+ "x": 1738.6949462890625,
+ "y": 547.072998046875
+ },
+ {
+ "x": 1664.5260009765625,
+ "y": 462.3299865722656
+ },
+ {
+ "x": 1664.5260009765625,
+ "y": 362.114990234375
+ },
+ {
+ "x": 1664.5260009765625,
+ "y": 261.8999938964844
+ },
+ {
+ "x": 1738.6949462890625,
+ "y": 177.15699768066406
+ },
+ {
+ "x": 1838.0260009765625,
+ "y": 163.8780059814453
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
new file mode 100644
index 0000000000..d7adc6a814
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
@@ -0,0 +1,40 @@
+abcdabcxyzabpq
+
+
+
\ 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..813ddcfcfe
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json
@@ -0,0 +1,1525 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "four",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 513,
+ "height": 526,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "four.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 230,
+ "y": 30
+ },
+ "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": "four.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 430,
+ "y": 230
+ },
+ "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": "four.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 230,
+ "y": 430
+ },
+ "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": "four.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 30,
+ "y": 230
+ },
+ "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": "nonrect",
+ "type": "cycle",
+ "pos": {
+ "x": 533,
+ "y": 0
+ },
+ "width": 500,
+ "height": 524,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "nonrect.a",
+ "type": "oval",
+ "pos": {
+ "x": 709,
+ "y": 30
+ },
+ "width": 120,
+ "height": 120,
+ "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": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 12,
+ "labelHeight": 32,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "nonrect.b",
+ "type": "hexagon",
+ "pos": {
+ "x": 882,
+ "y": 329
+ },
+ "width": 121,
+ "height": 121,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "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": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 14,
+ "labelHeight": 37,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "nonrect.c",
+ "type": "diamond",
+ "pos": {
+ "x": 573,
+ "y": 344
+ },
+ "width": 46,
+ "height": 92,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N4",
+ "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": "mixed",
+ "type": "cycle",
+ "pos": {
+ "x": 0,
+ "y": 546
+ },
+ "width": 559,
+ "height": 693,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "red",
+ "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": "mixed.x",
+ "type": "rectangle",
+ "pos": {
+ "x": 210,
+ "y": 576
+ },
+ "width": 174,
+ "height": 318,
+ "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": "x",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 11,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "mixed.x.p",
+ "type": "rectangle",
+ "pos": {
+ "x": 271,
+ "y": 636
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "p",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.x.q",
+ "type": "rectangle",
+ "pos": {
+ "x": 270,
+ "y": 768
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "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": "q",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "mixed.y",
+ "type": "rectangle",
+ "pos": {
+ "x": 475,
+ "y": 1057
+ },
+ "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": "y",
+ "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": "mixed.z",
+ "type": "oval",
+ "pos": {
+ "x": 53,
+ "y": 1051
+ },
+ "width": 77,
+ "height": 77,
+ "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": "z",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 7,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "two",
+ "type": "cycle",
+ "pos": {
+ "x": 580,
+ "y": 546
+ },
+ "width": 460,
+ "height": 580,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "two.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 750,
+ "y": 576
+ },
+ "width": 120,
+ "height": 120,
+ "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": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 14,
+ "labelHeight": 37,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "two.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 750,
+ "y": 976
+ },
+ "width": 120,
+ "height": 120,
+ "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": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 14,
+ "labelHeight": 37,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "four.(a -> b)[0]",
+ "src": "four.a",
+ "srcArrow": "none",
+ "dst": "four.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": 283.5,
+ "y": 64.76300048828125
+ },
+ {
+ "x": 370.5150146484375,
+ "y": 76.3949966430664
+ },
+ {
+ "x": 439.77301025390625,
+ "y": 143.41299438476562
+ },
+ {
+ "x": 454.25799560546875,
+ "y": 229.99899291992188
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(b -> c)[0]",
+ "src": "four.b",
+ "srcArrow": "none",
+ "dst": "four.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": 454.25799560546875,
+ "y": 296
+ },
+ {
+ "x": 439.77301025390625,
+ "y": 382.58599853515625
+ },
+ {
+ "x": 370.5150146484375,
+ "y": 449.60400390625
+ },
+ {
+ "x": 283.5,
+ "y": 461.2359924316406
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(c -> d)[0]",
+ "src": "four.c",
+ "srcArrow": "none",
+ "dst": "four.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": 230.49899291992188,
+ "y": 461.2359924316406
+ },
+ {
+ "x": 143.48399353027344,
+ "y": 449.60400390625
+ },
+ {
+ "x": 74.22599792480469,
+ "y": 382.58599853515625
+ },
+ {
+ "x": 59.74100112915039,
+ "y": 296
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "four.(d -> a)[0]",
+ "src": "four.d",
+ "srcArrow": "none",
+ "dst": "four.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": 59.74100112915039,
+ "y": 229.99899291992188
+ },
+ {
+ "x": 74.22599792480469,
+ "y": 143.41299438476562
+ },
+ {
+ "x": 143.48399353027344,
+ "y": 76.3949966430664
+ },
+ {
+ "x": 230.49899291992188,
+ "y": 64.76300048828125
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(a -> b)[0]",
+ "src": "nonrect.a",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 829.1439819335938,
+ "y": 99.00499725341797
+ },
+ {
+ "x": 927.260009765625,
+ "y": 129.47300720214844
+ },
+ {
+ "x": 986.3280029296875,
+ "y": 229.3280029296875
+ },
+ {
+ "x": 965.822998046875,
+ "y": 330
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> c)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 905.822998046875,
+ "y": 437
+ },
+ {
+ "x": 864.0809936523438,
+ "y": 475.260009765625
+ },
+ {
+ "x": 808.166015625,
+ "y": 494.29400634765625
+ },
+ {
+ "x": 751.7730102539062,
+ "y": 489.1830139160156
+ },
+ {
+ "x": 695.3809814453125,
+ "y": 484.072998046875
+ },
+ {
+ "x": 643.7969970703125,
+ "y": 455.2980041503906
+ },
+ {
+ "x": 609.822998046875,
+ "y": 410
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(c -> a)[0]",
+ "src": "nonrect.c",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 584.822998046875,
+ "y": 366
+ },
+ {
+ "x": 563.5,
+ "y": 314.08599853515625
+ },
+ {
+ "x": 564.9660034179688,
+ "y": 255.6020050048828
+ },
+ {
+ "x": 588.8670043945312,
+ "y": 204.82400512695312
+ },
+ {
+ "x": 612.7680053710938,
+ "y": 154.04600524902344
+ },
+ {
+ "x": 656.905029296875,
+ "y": 115.64600372314453
+ },
+ {
+ "x": 710.5029907226562,
+ "y": 99.00499725341797
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "nonrect.(b -> a)[0]",
+ "src": "nonrect.b",
+ "srcArrow": "none",
+ "dst": "nonrect.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": 965.822998046875,
+ "y": 330
+ },
+ {
+ "x": 986.3280029296875,
+ "y": 229.3280029296875
+ },
+ {
+ "x": 927.260009765625,
+ "y": 129.47300720214844
+ },
+ {
+ "x": 829.1439819335938,
+ "y": 99.00499725341797
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.x.(p -> q)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.x.q",
+ "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": 297.4649963378906,
+ "y": 702
+ },
+ {
+ "x": 297.4649963378906,
+ "y": 768
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(x.p -> y)[0]",
+ "src": "mixed.x.p",
+ "srcArrow": "none",
+ "dst": "mixed.y",
+ "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": 324.4649963378906,
+ "y": 669
+ },
+ {
+ "x": 475.7099914550781,
+ "y": 1090.4949951171875
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(y -> z)[0]",
+ "src": "mixed.y",
+ "srcArrow": "none",
+ "dst": "mixed.z",
+ "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": 479.7170104980469,
+ "y": 1123.4949951171875
+ },
+ {
+ "x": 434.49700927734375,
+ "y": 1177.89501953125
+ },
+ {
+ "x": 367.34100341796875,
+ "y": 1209.25
+ },
+ {
+ "x": 296.60198974609375,
+ "y": 1208.9930419921875
+ },
+ {
+ "x": 225.86199951171875,
+ "y": 1208.7349853515625
+ },
+ {
+ "x": 158.93600463867188,
+ "y": 1176.8909912109375
+ },
+ {
+ "x": 114.12000274658203,
+ "y": 1122.1590576171875
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "mixed.(z -> x)[0]",
+ "src": "mixed.z",
+ "srcArrow": "none",
+ "dst": "mixed.x",
+ "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": 75.74800109863281,
+ "y": 1055.697021484375
+ },
+ {
+ "x": 30,
+ "y": 934.5339965820312
+ },
+ {
+ "x": 89.99500274658203,
+ "y": 799.0880126953125
+ },
+ {
+ "x": 210.46499633789062,
+ "y": 751.5460205078125
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(a -> b)[0]",
+ "src": "two.a",
+ "srcArrow": "none",
+ "dst": "two.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": 870,
+ "y": 645.2119750976562
+ },
+ {
+ "x": 953.3200073242188,
+ "y": 671.4149780273438
+ },
+ {
+ "x": 1010,
+ "y": 748.656005859375
+ },
+ {
+ "x": 1010,
+ "y": 836
+ },
+ {
+ "x": 1010,
+ "y": 923.343017578125
+ },
+ {
+ "x": 953.3200073242188,
+ "y": 1000.583984375
+ },
+ {
+ "x": 870,
+ "y": 1026.7869873046875
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "two.(b -> a)[0]",
+ "src": "two.b",
+ "srcArrow": "none",
+ "dst": "two.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": 749.9990234375,
+ "y": 1026.7869873046875
+ },
+ {
+ "x": 666.6790161132812,
+ "y": 1000.583984375
+ },
+ {
+ "x": 610,
+ "y": 923.343017578125
+ },
+ {
+ "x": 610,
+ "y": 836
+ },
+ {
+ "x": 610,
+ "y": 748.656005859375
+ },
+ {
+ "x": 666.6790161132812,
+ "y": 671.4149780273438
+ },
+ {
+ "x": 749.9990234375,
+ "y": 645.2119750976562
+ }
+ ],
+ "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/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
new file mode 100644
index 0000000000..adac8f79f3
--- /dev/null
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
@@ -0,0 +1,40 @@
+abcdabcxyzabpq
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index 7585d91b3e..15e8ff8657 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -1773,3 +1773,35 @@ style: {fill-pattern: dots; fill:"radial-gradient(#fbfbf8, #e3e3f0)"; stroke: "#
a->b
+-- cycle-diagram --
+four: "" {
+ shape: cycle
+ a -> b -> c -> d -> a
+}
+nonrect: "" {
+ shape: cycle
+ a: {shape: circle}
+ b: {shape: hexagon}
+ c: {shape: diamond}
+ a -> b -> c -> a
+ b -> a
+}
+mixed: "" {
+ shape: cycle
+ style.fill: honeydew
+ style.stroke: red
+ x: {
+ p
+ q
+ p -> q
+ }
+ y
+ z: {shape: circle}
+ x.p -> y
+ y -> z -> x
+}
+two: "" {
+ shape: cycle
+ a -> b
+ b -> a
+}