From 3d2a4dd8fd87461c8714feaad831af7b68ca82cb Mon Sep 17 00:00:00 2001
From: Furox-Art <177975472+Furox-Art@users.noreply.github.com>
Date: Mon, 14 Sep 2026 02:12:16 +0300
Subject: [PATCH 1/2] feat: add cycle shape
shape: cycle arranges its children with their centers on a circle and
routes edges between them along that circle as exact circular arcs
(cubic Bezier chains using the canonical arc control offset), with
endpoints trimmed to each child's visible border so arrowheads touch
rectangles, circles, polygons alike.
- children keep their internal layout: the core layout runs first and
containers are moved onto the ring together with their descendants,
translating internal and cross-child edge routes along
- root-level edges take the shorter way around the ring; on a two-node
cycle opposite edges take opposite semicircles
- the container renders like a container (styles apply), in both d2svg
and d2scenebuild
- e2e txtar fixture cycle-diagram with dagre, elk and tala goldens
---
ci/release/changelogs/next.md | 3 +
d2graph/cyclediagram.go | 7 +
d2graph/d2graph.go | 2 +-
d2layouts/d2cycle/layout.go | 493 ++++++
d2layouts/d2cycle/layout_test.go | 281 +++
d2layouts/d2layouts.go | 10 +
d2renderers/d2scenebuild/shape_effects.go | 2 +-
d2renderers/d2scenebuild/sketch_shapes.go | 6 +-
d2renderers/d2svg/d2svg.go | 2 +-
d2target/d2target.go | 3 +
.../txtar/cycle-diagram/dagre/board.exp.json | 1567 +++++++++++++++++
.../txtar/cycle-diagram/dagre/sketch.exp.svg | 40 +
.../txtar/cycle-diagram/elk/board.exp.json | 1565 ++++++++++++++++
.../txtar/cycle-diagram/elk/sketch.exp.svg | 40 +
.../txtar/cycle-diagram/tala/board.exp.json | 1525 ++++++++++++++++
.../txtar/cycle-diagram/tala/sketch.exp.svg | 40 +
e2etests/txtar.txt | 32 +
17 files changed, 5612 insertions(+), 6 deletions(-)
create mode 100644 d2graph/cyclediagram.go
create mode 100644 d2layouts/d2cycle/layout.go
create mode 100644 d2layouts/d2cycle/layout_test.go
create mode 100644 e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
create mode 100644 e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
create mode 100644 e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
create mode 100644 e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
create mode 100644 e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json
create mode 100644 e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
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..e20507fa29
--- /dev/null
+++ b/d2layouts/d2cycle/layout.go
@@ -0,0 +1,493 @@
+// 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.
+ // 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
+ }
+ }
+
+ 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 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
+ }
+
+ dx := -tl.X
+ dy := -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, br.Y-tl.Y)
+}
+
+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..140180424d
--- /dev/null
+++ b/d2layouts/d2cycle/layout_test.go
@@ -0,0 +1,281 @@
+package d2cycle
+
+import (
+ "context"
+ "math"
+ "strings"
+ "testing"
+
+ "github.com/d2lang/d2/d2compiler"
+ "github.com/d2lang/d2/d2graph"
+ "github.com/d2lang/d2/lib/geo"
+)
+
+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 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..d97747f4c0
--- /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": 453,
+ "height": 466,
+ "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": 200,
+ "y": 62
+ },
+ "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": 400,
+ "y": 262
+ },
+ "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": 200,
+ "y": 462
+ },
+ "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": 0,
+ "y": 262
+ },
+ "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": 513,
+ "y": 76
+ },
+ "width": 415,
+ "height": 439,
+ "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": 682,
+ "y": 76
+ },
+ "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": 868,
+ "y": 380
+ },
+ "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": 524,
+ "y": 368
+ },
+ "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": 988,
+ "y": 0
+ },
+ "width": 468,
+ "height": 590,
+ "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": 1153,
+ "y": 0
+ },
+ "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": 1237,
+ "y": 30
+ },
+ "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": 1183,
+ "y": 196
+ },
+ "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": 1402,
+ "y": 445
+ },
+ "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": 1006,
+ "y": 440
+ },
+ "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": 1516,
+ "y": 62
+ },
+ "width": 400,
+ "height": 466,
+ "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": 1689,
+ "y": 62
+ },
+ "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": 1689,
+ "y": 462
+ },
+ "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": 253.5,
+ "y": 96.76300048828125
+ },
+ {
+ "x": 340.5150146484375,
+ "y": 108.3949966430664
+ },
+ {
+ "x": 409.77301025390625,
+ "y": 175.41299438476562
+ },
+ {
+ "x": 424.25799560546875,
+ "y": 261.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": 424.25799560546875,
+ "y": 328
+ },
+ {
+ "x": 409.77301025390625,
+ "y": 414.58599853515625
+ },
+ {
+ "x": 340.5150146484375,
+ "y": 481.60400390625
+ },
+ {
+ "x": 253.5,
+ "y": 493.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": 200.49899291992188,
+ "y": 493.2359924316406
+ },
+ {
+ "x": 113.48400115966797,
+ "y": 481.60400390625
+ },
+ {
+ "x": 44.22600173950195,
+ "y": 414.58599853515625
+ },
+ {
+ "x": 29.740999221801758,
+ "y": 328
+ }
+ ],
+ "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": 29.740999221801758,
+ "y": 261.9989929199219
+ },
+ {
+ "x": 44.22600173950195,
+ "y": 175.41299438476562
+ },
+ {
+ "x": 113.48400115966797,
+ "y": 108.3949966430664
+ },
+ {
+ "x": 200.49899291992188,
+ "y": 96.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": 759.0819702148438,
+ "y": 118.20600128173828
+ },
+ {
+ "x": 816.791015625,
+ "y": 129.4709930419922
+ },
+ {
+ "x": 866.6190185546875,
+ "y": 165.56100463867188
+ },
+ {
+ "x": 895.3189697265625,
+ "y": 216.8800048828125
+ },
+ {
+ "x": 924.0180053710938,
+ "y": 268.1990051269531
+ },
+ {
+ "x": 928.6810302734375,
+ "y": 329.5469970703125
+ },
+ {
+ "x": 907.760986328125,
+ "y": 384.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": 877.760986328125,
+ "y": 438.5
+ },
+ {
+ "x": 839.239013671875,
+ "y": 487.1719970703125
+ },
+ {
+ "x": 780.3289794921875,
+ "y": 515.260009765625
+ },
+ {
+ "x": 718.260986328125,
+ "y": 514.4840087890625
+ },
+ {
+ "x": 656.1939697265625,
+ "y": 513.7080078125
+ },
+ {
+ "x": 598.0040283203125,
+ "y": 484.1579895019531
+ },
+ {
+ "x": 560.760986328125,
+ "y": 434.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": 535.760986328125,
+ "y": 390.5
+ },
+ {
+ "x": 513,
+ "y": 335.0880126953125
+ },
+ {
+ "x": 516.27197265625,
+ "y": 272.3909912109375
+ },
+ {
+ "x": 544.6820068359375,
+ "y": 219.6510009765625
+ },
+ {
+ "x": 573.0910034179688,
+ "y": 166.91099548339844
+ },
+ {
+ "x": 623.64501953125,
+ "y": 129.68299865722656
+ },
+ {
+ "x": 682.4390258789062,
+ "y": 118.20600128173828
+ }
+ ],
+ "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": 907.760986328125,
+ "y": 384.5
+ },
+ {
+ "x": 928.6810302734375,
+ "y": 329.5469970703125
+ },
+ {
+ "x": 924.0180053710938,
+ "y": 268.1990051269531
+ },
+ {
+ "x": 895.3189697265625,
+ "y": 216.8800048828125
+ },
+ {
+ "x": 866.6190185546875,
+ "y": 165.56100463867188
+ },
+ {
+ "x": 816.791015625,
+ "y": 129.4709930419922
+ },
+ {
+ "x": 759.0819702148438,
+ "y": 118.20600128173828
+ }
+ ],
+ "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": 1242.9300537109375,
+ "y": 96
+ },
+ {
+ "x": 1217.3299560546875,
+ "y": 136
+ },
+ {
+ "x": 1210.9300537109375,
+ "y": 156
+ },
+ {
+ "x": 1210.9300537109375,
+ "y": 196
+ }
+ ],
+ "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": 1277.9300537109375,
+ "y": 96
+ },
+ {
+ "x": 1387.4300537109375,
+ "y": 523.9229736328125
+ },
+ {
+ "x": 1406.030029296875,
+ "y": 545.9229736328125
+ },
+ {
+ "x": 1402.6209716796875,
+ "y": 455.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": 1406.322998046875,
+ "y": 511.8840026855469
+ },
+ {
+ "x": 1363.9429931640625,
+ "y": 561.60400390625
+ },
+ {
+ "x": 1301.8089599609375,
+ "y": 590.1240234375
+ },
+ {
+ "x": 1236.47900390625,
+ "y": 589.843994140625
+ },
+ {
+ "x": 1171.14794921875,
+ "y": 589.5640258789062
+ },
+ {
+ "x": 1109.260986328125,
+ "y": 560.5120239257812
+ },
+ {
+ "x": 1067.31005859375,
+ "y": 510.42999267578125
+ }
+ ],
+ "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": 1028.9549560546875,
+ "y": 443.99798583984375
+ },
+ {
+ "x": 988,
+ "y": 331.76800537109375
+ },
+ {
+ "x": 1043.239990234375,
+ "y": 207.25799560546875
+ },
+ {
+ "x": 1153.9300537109375,
+ "y": 162.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": 1742.5,
+ "y": 96.76300048828125
+ },
+ {
+ "x": 1841.8310546875,
+ "y": 110.04100036621094
+ },
+ {
+ "x": 1916,
+ "y": 194.78500366210938
+ },
+ {
+ "x": 1916,
+ "y": 295
+ },
+ {
+ "x": 1916,
+ "y": 395.2139892578125
+ },
+ {
+ "x": 1841.8310546875,
+ "y": 479.9580078125
+ },
+ {
+ "x": 1742.5,
+ "y": 493.2359924316406
+ }
+ ],
+ "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": 1689.4990234375,
+ "y": 493.2359924316406
+ },
+ {
+ "x": 1590.16796875,
+ "y": 479.9580078125
+ },
+ {
+ "x": 1516,
+ "y": 395.2139892578125
+ },
+ {
+ "x": 1516,
+ "y": 295
+ },
+ {
+ "x": 1516,
+ "y": 194.78500366210938
+ },
+ {
+ "x": 1590.16796875,
+ "y": 110.04100036621094
+ },
+ {
+ "x": 1689.4990234375,
+ "y": 96.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..a521f90259
--- /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..ddd043e73a
--- /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": 454,
+ "height": 466,
+ "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": 212,
+ "y": 99
+ },
+ "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": 412,
+ "y": 299
+ },
+ "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": 212,
+ "y": 499
+ },
+ "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": 12,
+ "y": 299
+ },
+ "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": 485,
+ "y": 111
+ },
+ "width": 421,
+ "height": 441,
+ "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": 653,
+ "y": 111
+ },
+ "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": 826,
+ "y": 415
+ },
+ "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": 496,
+ "y": 404
+ },
+ "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": 926,
+ "y": 12
+ },
+ "width": 509,
+ "height": 641,
+ "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": 1097,
+ "y": 12
+ },
+ "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": 1167,
+ "y": 62
+ },
+ "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": 1147,
+ "y": 208
+ },
+ "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": 1380,
+ "y": 497
+ },
+ "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": 949,
+ "y": 492
+ },
+ "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": 1454,
+ "y": 99
+ },
+ "width": 400,
+ "height": 466,
+ "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": 1628,
+ "y": 99
+ },
+ "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": 1628,
+ "y": 499
+ },
+ "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": 265.5,
+ "y": 133.8780059814453
+ },
+ {
+ "x": 352.5150146484375,
+ "y": 145.50999450683594
+ },
+ {
+ "x": 421.77301025390625,
+ "y": 212.5279998779297
+ },
+ {
+ "x": 436.25799560546875,
+ "y": 299.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": 436.25799560546875,
+ "y": 365.114990234375
+ },
+ {
+ "x": 421.77301025390625,
+ "y": 451.70098876953125
+ },
+ {
+ "x": 352.5150146484375,
+ "y": 518.718994140625
+ },
+ {
+ "x": 265.5,
+ "y": 530.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": 212.49899291992188,
+ "y": 530.3510131835938
+ },
+ {
+ "x": 125.48400115966797,
+ "y": 518.718994140625
+ },
+ {
+ "x": 56.22600173950195,
+ "y": 451.70098876953125
+ },
+ {
+ "x": 41.74100112915039,
+ "y": 365.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": 41.74100112915039,
+ "y": 299.114990234375
+ },
+ {
+ "x": 56.22600173950195,
+ "y": 212.5279998779297
+ },
+ {
+ "x": 125.48400115966797,
+ "y": 145.50999450683594
+ },
+ {
+ "x": 212.49899291992188,
+ "y": 133.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": 732.9439697265625,
+ "y": 154.35800170898438
+ },
+ {
+ "x": 789.5540161132812,
+ "y": 165.84100341796875
+ },
+ {
+ "x": 838.4299926757812,
+ "y": 201.24400329589844
+ },
+ {
+ "x": 866.9940185546875,
+ "y": 251.44900512695312
+ },
+ {
+ "x": 895.5590209960938,
+ "y": 301.65399169921875
+ },
+ {
+ "x": 901.0230102539062,
+ "y": 361.7569885253906
+ },
+ {
+ "x": 882.1610107421875,
+ "y": 416.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": 844.1610107421875,
+ "y": 481.35400390625
+ },
+ {
+ "x": 804.6160278320312,
+ "y": 527.0809936523438
+ },
+ {
+ "x": 746.469970703125,
+ "y": 552.3759765625
+ },
+ {
+ "x": 686.0540161132812,
+ "y": 550.2269897460938
+ },
+ {
+ "x": 625.6370239257812,
+ "y": 548.0789794921875
+ },
+ {
+ "x": 569.4340209960938,
+ "y": 518.718017578125
+ },
+ {
+ "x": 533.1610107421875,
+ "y": 470.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": 508.1610107421875,
+ "y": 426.35400390625
+ },
+ {
+ "x": 485.5,
+ "y": 371.1830139160156
+ },
+ {
+ "x": 488.6409912109375,
+ "y": 308.77801513671875
+ },
+ {
+ "x": 516.72998046875,
+ "y": 256.1629943847656
+ },
+ {
+ "x": 544.8189697265625,
+ "y": 203.54800415039062
+ },
+ {
+ "x": 594.9249877929688,
+ "y": 166.21499633789062
+ },
+ {
+ "x": 653.3779907226562,
+ "y": 154.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": 882.1610107421875,
+ "y": 416.35400390625
+ },
+ {
+ "x": 901.0230102539062,
+ "y": 361.7569885253906
+ },
+ {
+ "x": 895.5590209960938,
+ "y": 301.65399169921875
+ },
+ {
+ "x": 866.9940185546875,
+ "y": 251.44900512695312
+ },
+ {
+ "x": 838.4299926757812,
+ "y": 201.24400329589844
+ },
+ {
+ "x": 789.5540161132812,
+ "y": 165.84100341796875
+ },
+ {
+ "x": 732.9439697265625,
+ "y": 154.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": 1194.623046875,
+ "y": 128
+ },
+ {
+ "x": 1194.623046875,
+ "y": 168
+ },
+ {
+ "x": 1174.4560546875,
+ "y": 168
+ },
+ {
+ "x": 1174.4560546875,
+ "y": 208
+ }
+ ],
+ "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": 1221.2889404296875,
+ "y": 128
+ },
+ {
+ "x": 1333.7889404296875,
+ "y": 617.989990234375
+ },
+ {
+ "x": 1353.9560546875,
+ "y": 617.989990234375
+ },
+ {
+ "x": 1407.5260009765625,
+ "y": 497.9859924316406
+ }
+ ],
+ "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": 1384.6240234375,
+ "y": 563.9860229492188
+ },
+ {
+ "x": 1338.468017578125,
+ "y": 619.9349975585938
+ },
+ {
+ "x": 1269.6490478515625,
+ "y": 652.22998046875
+ },
+ {
+ "x": 1197.1190185546875,
+ "y": 651.97900390625
+ },
+ {
+ "x": 1124.5889892578125,
+ "y": 651.72900390625
+ },
+ {
+ "x": 1055.9959716796875,
+ "y": 618.9580078125
+ },
+ {
+ "x": 1010.2310180664062,
+ "y": 562.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": 971.85302734375,
+ "y": 496.21600341796875
+ },
+ {
+ "x": 926.3660278320312,
+ "y": 376.9519958496094
+ },
+ {
+ "x": 981.3410034179688,
+ "y": 242.86700439453125
+ },
+ {
+ "x": 1097.4560546875,
+ "y": 189.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": 1681.0260009765625,
+ "y": 133.8780059814453
+ },
+ {
+ "x": 1780.3570556640625,
+ "y": 147.15699768066406
+ },
+ {
+ "x": 1854.5260009765625,
+ "y": 231.89999389648438
+ },
+ {
+ "x": 1854.5260009765625,
+ "y": 332.114990234375
+ },
+ {
+ "x": 1854.5260009765625,
+ "y": 432.3299865722656
+ },
+ {
+ "x": 1780.3570556640625,
+ "y": 517.072998046875
+ },
+ {
+ "x": 1681.0260009765625,
+ "y": 530.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": 1628.0260009765625,
+ "y": 530.3510131835938
+ },
+ {
+ "x": 1528.6949462890625,
+ "y": 517.072998046875
+ },
+ {
+ "x": 1454.5260009765625,
+ "y": 432.3299865722656
+ },
+ {
+ "x": 1454.5260009765625,
+ "y": 332.114990234375
+ },
+ {
+ "x": 1454.5260009765625,
+ "y": 231.89999389648438
+ },
+ {
+ "x": 1528.6949462890625,
+ "y": 147.15699768066406
+ },
+ {
+ "x": 1628.0260009765625,
+ "y": 133.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..0502fbe56d
--- /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..52d9ba2a41
--- /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": 453,
+ "height": 466,
+ "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": 200,
+ "y": 0
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "four.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 400,
+ "y": 200
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "b",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "four.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 200,
+ "y": 400
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "c",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "four.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 200
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "nonrect",
+ "type": "cycle",
+ "pos": {
+ "x": 473,
+ "y": 0
+ },
+ "width": 440,
+ "height": 464,
+ "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": 619,
+ "y": 0
+ },
+ "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": 792,
+ "y": 299
+ },
+ "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": 483,
+ "y": 314
+ },
+ "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": 486
+ },
+ "width": 499,
+ "height": 633,
+ "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": 180,
+ "y": 486
+ },
+ "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": 241,
+ "y": 546
+ },
+ "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": 240,
+ "y": 678
+ },
+ "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": 445,
+ "y": 967
+ },
+ "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": 23,
+ "y": 961
+ },
+ "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": 520,
+ "y": 486
+ },
+ "width": 400,
+ "height": 520,
+ "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": 660,
+ "y": 486
+ },
+ "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": 660,
+ "y": 886
+ },
+ "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": 253.5,
+ "y": 34.76300048828125
+ },
+ {
+ "x": 340.5150146484375,
+ "y": 46.39500045776367
+ },
+ {
+ "x": 409.77301025390625,
+ "y": 113.41300201416016
+ },
+ {
+ "x": 424.25799560546875,
+ "y": 199.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": 424.25799560546875,
+ "y": 266
+ },
+ {
+ "x": 409.77301025390625,
+ "y": 352.58599853515625
+ },
+ {
+ "x": 340.5150146484375,
+ "y": 419.60400390625
+ },
+ {
+ "x": 253.5,
+ "y": 431.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": 200.49899291992188,
+ "y": 431.2359924316406
+ },
+ {
+ "x": 113.48400115966797,
+ "y": 419.60400390625
+ },
+ {
+ "x": 44.22600173950195,
+ "y": 352.58599853515625
+ },
+ {
+ "x": 29.740999221801758,
+ "y": 266
+ }
+ ],
+ "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": 29.740999221801758,
+ "y": 199.99899291992188
+ },
+ {
+ "x": 44.22600173950195,
+ "y": 113.41300201416016
+ },
+ {
+ "x": 113.48400115966797,
+ "y": 46.39500045776367
+ },
+ {
+ "x": 200.49899291992188,
+ "y": 34.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": 739.1439819335938,
+ "y": 69.00499725341797
+ },
+ {
+ "x": 837.260009765625,
+ "y": 99.4729995727539
+ },
+ {
+ "x": 896.3280029296875,
+ "y": 199.3280029296875
+ },
+ {
+ "x": 875.822998046875,
+ "y": 300
+ }
+ ],
+ "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": 815.822998046875,
+ "y": 407
+ },
+ {
+ "x": 774.0809936523438,
+ "y": 445.260009765625
+ },
+ {
+ "x": 718.166015625,
+ "y": 464.29400634765625
+ },
+ {
+ "x": 661.7730102539062,
+ "y": 459.1830139160156
+ },
+ {
+ "x": 605.3809814453125,
+ "y": 454.072998046875
+ },
+ {
+ "x": 553.7969970703125,
+ "y": 425.2980041503906
+ },
+ {
+ "x": 519.822998046875,
+ "y": 380
+ }
+ ],
+ "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": 494.822998046875,
+ "y": 336
+ },
+ {
+ "x": 473.5,
+ "y": 284.08599853515625
+ },
+ {
+ "x": 474.96600341796875,
+ "y": 225.6020050048828
+ },
+ {
+ "x": 498.86700439453125,
+ "y": 174.82400512695312
+ },
+ {
+ "x": 522.7680053710938,
+ "y": 124.0459976196289
+ },
+ {
+ "x": 566.905029296875,
+ "y": 85.64600372314453
+ },
+ {
+ "x": 620.5029907226562,
+ "y": 69.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": 875.822998046875,
+ "y": 300
+ },
+ {
+ "x": 896.3280029296875,
+ "y": 199.3280029296875
+ },
+ {
+ "x": 837.260009765625,
+ "y": 99.4729995727539
+ },
+ {
+ "x": 739.1439819335938,
+ "y": 69.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": 267.4649963378906,
+ "y": 612
+ },
+ {
+ "x": 267.4649963378906,
+ "y": 678
+ }
+ ],
+ "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": 294.4649963378906,
+ "y": 579
+ },
+ {
+ "x": 445.7099914550781,
+ "y": 1000.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": 449.7170104980469,
+ "y": 1033.4949951171875
+ },
+ {
+ "x": 404.49700927734375,
+ "y": 1087.89501953125
+ },
+ {
+ "x": 337.34100341796875,
+ "y": 1119.25
+ },
+ {
+ "x": 266.60198974609375,
+ "y": 1118.9930419921875
+ },
+ {
+ "x": 195.86199951171875,
+ "y": 1118.7349853515625
+ },
+ {
+ "x": 128.93600463867188,
+ "y": 1086.8909912109375
+ },
+ {
+ "x": 84.12000274658203,
+ "y": 1032.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": 45.74800109863281,
+ "y": 965.697021484375
+ },
+ {
+ "x": 0,
+ "y": 844.5339965820312
+ },
+ {
+ "x": 59.994998931884766,
+ "y": 709.0880126953125
+ },
+ {
+ "x": 180.46499633789062,
+ "y": 661.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": 780,
+ "y": 555.2119750976562
+ },
+ {
+ "x": 863.3200073242188,
+ "y": 581.4149780273438
+ },
+ {
+ "x": 920,
+ "y": 658.656005859375
+ },
+ {
+ "x": 920,
+ "y": 746
+ },
+ {
+ "x": 920,
+ "y": 833.343017578125
+ },
+ {
+ "x": 863.3200073242188,
+ "y": 910.583984375
+ },
+ {
+ "x": 780,
+ "y": 936.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": 659.9990234375,
+ "y": 936.7869873046875
+ },
+ {
+ "x": 576.6790161132812,
+ "y": 910.583984375
+ },
+ {
+ "x": 520,
+ "y": 833.343017578125
+ },
+ {
+ "x": 520,
+ "y": 746
+ },
+ {
+ "x": 520,
+ "y": 658.656005859375
+ },
+ {
+ "x": 576.6790161132812,
+ "y": 581.4149780273438
+ },
+ {
+ "x": 659.9990234375,
+ "y": 555.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..d4e4db7928
--- /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
+}
From f2f2863d8210156dd72110c3fd93eede826228cf Mon Sep 17 00:00:00 2001
From: Furox-Art <177975472+Furox-Art@users.noreply.github.com>
Date: Thu, 24 Sep 2026 01:31:20 +0300
Subject: [PATCH 2/2] fix: inset cycle container content
---
d2layouts/d2cycle/layout.go | 35 +-
d2layouts/d2cycle/layout_test.go | 38 ++
.../txtar/cycle-diagram/dagre/board.exp.json | 386 +++++++++---------
.../txtar/cycle-diagram/dagre/sketch.exp.svg | 36 +-
.../txtar/cycle-diagram/elk/board.exp.json | 386 +++++++++---------
.../txtar/cycle-diagram/elk/sketch.exp.svg | 36 +-
.../txtar/cycle-diagram/tala/board.exp.json | 348 ++++++++--------
.../txtar/cycle-diagram/tala/sketch.exp.svg | 36 +-
8 files changed, 682 insertions(+), 619 deletions(-)
diff --git a/d2layouts/d2cycle/layout.go b/d2layouts/d2cycle/layout.go
index e20507fa29..ec48174369 100644
--- a/d2layouts/d2cycle/layout.go
+++ b/d2layouts/d2cycle/layout.go
@@ -21,7 +21,8 @@ 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.
+ padding = 24.
+ containerPadding = 30.
// maxArcSweep bounds each cubic segment so the Bezier approximation of a
// circular arc stays visually exact.
maxArcSweep = math.Pi / 2
@@ -51,6 +52,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, coreLayout d2graph.LayoutGrap
}
}
+ positionLabelsIcons(g.Root)
for _, obj := range g.Objects {
positionLabelsIcons(obj)
}
@@ -395,7 +397,7 @@ func pointOnCircle(center *geo.Point, radius, angle float64) *geo.Point {
}
// normalizeGraph shifts all objects and routes so the bounding box starts at
-// (0, 0) and records the bounding box on the root.
+// (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))
@@ -422,8 +424,9 @@ func normalizeGraph(g *d2graph.Graph) {
return
}
- dx := -tl.X
- dy := -tl.Y
+ 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 {
@@ -435,7 +438,29 @@ func normalizeGraph(g *d2graph.Graph) {
edge.Move(dx, dy)
}
}
- g.Root.Box = geo.NewBox(geo.NewPoint(0, 0), br.X-tl.X, br.Y-tl.Y)
+ 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 {
diff --git a/d2layouts/d2cycle/layout_test.go b/d2layouts/d2cycle/layout_test.go
index 140180424d..9125c0e637 100644
--- a/d2layouts/d2cycle/layout_test.go
+++ b/d2layouts/d2cycle/layout_test.go
@@ -8,7 +8,9 @@ import (
"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 {
@@ -123,6 +125,42 @@ a -> b -> c -> d -> a
}
}
+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
diff --git a/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
index d97747f4c0..5648bcd0dc 100644
--- a/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
@@ -19,8 +19,8 @@
"x": 0,
"y": 62
},
- "width": 453,
- "height": 466,
+ "width": 513,
+ "height": 526,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -57,8 +57,8 @@
"id": "four.a",
"type": "rectangle",
"pos": {
- "x": 200,
- "y": 62
+ "x": 230,
+ "y": 92
},
"width": 53,
"height": 66,
@@ -99,8 +99,8 @@
"id": "four.b",
"type": "rectangle",
"pos": {
- "x": 400,
- "y": 262
+ "x": 430,
+ "y": 292
},
"width": 53,
"height": 66,
@@ -141,8 +141,8 @@
"id": "four.c",
"type": "rectangle",
"pos": {
- "x": 200,
- "y": 462
+ "x": 230,
+ "y": 492
},
"width": 53,
"height": 66,
@@ -183,8 +183,8 @@
"id": "four.d",
"type": "rectangle",
"pos": {
- "x": 0,
- "y": 262
+ "x": 30,
+ "y": 292
},
"width": 54,
"height": 66,
@@ -225,11 +225,11 @@
"id": "nonrect",
"type": "cycle",
"pos": {
- "x": 513,
+ "x": 573,
"y": 76
},
- "width": 415,
- "height": 439,
+ "width": 475,
+ "height": 499,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -266,8 +266,8 @@
"id": "nonrect.a",
"type": "oval",
"pos": {
- "x": 682,
- "y": 76
+ "x": 772,
+ "y": 106
},
"width": 77,
"height": 77,
@@ -308,8 +308,8 @@
"id": "nonrect.b",
"type": "hexagon",
"pos": {
- "x": 868,
- "y": 380
+ "x": 958,
+ "y": 410
},
"width": 50,
"height": 69,
@@ -350,8 +350,8 @@
"id": "nonrect.c",
"type": "diamond",
"pos": {
- "x": 524,
- "y": 368
+ "x": 614,
+ "y": 398
},
"width": 46,
"height": 92,
@@ -392,11 +392,11 @@
"id": "mixed",
"type": "cycle",
"pos": {
- "x": 988,
+ "x": 1108,
"y": 0
},
- "width": 468,
- "height": 590,
+ "width": 528,
+ "height": 650,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -433,8 +433,8 @@
"id": "mixed.x",
"type": "rectangle",
"pos": {
- "x": 1153,
- "y": 0
+ "x": 1303,
+ "y": 30
},
"width": 167,
"height": 292,
@@ -475,8 +475,8 @@
"id": "mixed.x.p",
"type": "rectangle",
"pos": {
- "x": 1237,
- "y": 30
+ "x": 1387,
+ "y": 60
},
"width": 53,
"height": 66,
@@ -517,8 +517,8 @@
"id": "mixed.x.q",
"type": "rectangle",
"pos": {
- "x": 1183,
- "y": 196
+ "x": 1333,
+ "y": 226
},
"width": 54,
"height": 66,
@@ -559,8 +559,8 @@
"id": "mixed.y",
"type": "rectangle",
"pos": {
- "x": 1402,
- "y": 445
+ "x": 1552,
+ "y": 475
},
"width": 54,
"height": 66,
@@ -601,8 +601,8 @@
"id": "mixed.z",
"type": "oval",
"pos": {
- "x": 1006,
- "y": 440
+ "x": 1156,
+ "y": 470
},
"width": 77,
"height": 77,
@@ -643,11 +643,11 @@
"id": "two",
"type": "cycle",
"pos": {
- "x": 1516,
+ "x": 1696,
"y": 62
},
- "width": 400,
- "height": 466,
+ "width": 460,
+ "height": 526,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -684,8 +684,8 @@
"id": "two.a",
"type": "rectangle",
"pos": {
- "x": 1689,
- "y": 62
+ "x": 1899,
+ "y": 92
},
"width": 53,
"height": 66,
@@ -726,8 +726,8 @@
"id": "two.b",
"type": "rectangle",
"pos": {
- "x": 1689,
- "y": 462
+ "x": 1899,
+ "y": 492
},
"width": 53,
"height": 66,
@@ -792,20 +792,20 @@
"link": "",
"route": [
{
- "x": 253.5,
- "y": 96.76300048828125
+ "x": 283.5,
+ "y": 126.76300048828125
},
{
- "x": 340.5150146484375,
- "y": 108.3949966430664
+ "x": 370.5150146484375,
+ "y": 138.39500427246094
},
{
- "x": 409.77301025390625,
- "y": 175.41299438476562
+ "x": 439.77301025390625,
+ "y": 205.41299438476562
},
{
- "x": 424.25799560546875,
- "y": 261.9989929199219
+ "x": 454.25799560546875,
+ "y": 291.9989929199219
}
],
"isCurve": true,
@@ -840,20 +840,20 @@
"link": "",
"route": [
{
- "x": 424.25799560546875,
- "y": 328
+ "x": 454.25799560546875,
+ "y": 358
},
{
- "x": 409.77301025390625,
- "y": 414.58599853515625
+ "x": 439.77301025390625,
+ "y": 444.58599853515625
},
{
- "x": 340.5150146484375,
- "y": 481.60400390625
+ "x": 370.5150146484375,
+ "y": 511.60400390625
},
{
- "x": 253.5,
- "y": 493.2359924316406
+ "x": 283.5,
+ "y": 523.2360229492188
}
],
"isCurve": true,
@@ -888,20 +888,20 @@
"link": "",
"route": [
{
- "x": 200.49899291992188,
- "y": 493.2359924316406
+ "x": 230.49899291992188,
+ "y": 523.2360229492188
},
{
- "x": 113.48400115966797,
- "y": 481.60400390625
+ "x": 143.48399353027344,
+ "y": 511.60400390625
},
{
- "x": 44.22600173950195,
- "y": 414.58599853515625
+ "x": 74.22599792480469,
+ "y": 444.58599853515625
},
{
- "x": 29.740999221801758,
- "y": 328
+ "x": 59.74100112915039,
+ "y": 358
}
],
"isCurve": true,
@@ -936,20 +936,20 @@
"link": "",
"route": [
{
- "x": 29.740999221801758,
- "y": 261.9989929199219
+ "x": 59.74100112915039,
+ "y": 291.9989929199219
},
{
- "x": 44.22600173950195,
- "y": 175.41299438476562
+ "x": 74.22599792480469,
+ "y": 205.41299438476562
},
{
- "x": 113.48400115966797,
- "y": 108.3949966430664
+ "x": 143.48399353027344,
+ "y": 138.39500427246094
},
{
- "x": 200.49899291992188,
- "y": 96.76300048828125
+ "x": 230.49899291992188,
+ "y": 126.76300048828125
}
],
"isCurve": true,
@@ -984,32 +984,32 @@
"link": "",
"route": [
{
- "x": 759.0819702148438,
- "y": 118.20600128173828
+ "x": 849.0819702148438,
+ "y": 148.20599365234375
},
{
- "x": 816.791015625,
- "y": 129.4709930419922
+ "x": 906.791015625,
+ "y": 159.4709930419922
},
{
- "x": 866.6190185546875,
- "y": 165.56100463867188
+ "x": 956.6190185546875,
+ "y": 195.56100463867188
},
{
- "x": 895.3189697265625,
- "y": 216.8800048828125
+ "x": 985.3189697265625,
+ "y": 246.8800048828125
},
{
- "x": 924.0180053710938,
- "y": 268.1990051269531
+ "x": 1014.0180053710938,
+ "y": 298.1990051269531
},
{
- "x": 928.6810302734375,
- "y": 329.5469970703125
+ "x": 1018.6810302734375,
+ "y": 359.5469970703125
},
{
- "x": 907.760986328125,
- "y": 384.5
+ "x": 997.760986328125,
+ "y": 414.5
}
],
"isCurve": true,
@@ -1044,32 +1044,32 @@
"link": "",
"route": [
{
- "x": 877.760986328125,
- "y": 438.5
+ "x": 967.760986328125,
+ "y": 468.5
},
{
- "x": 839.239013671875,
- "y": 487.1719970703125
+ "x": 929.239013671875,
+ "y": 517.1719970703125
},
{
- "x": 780.3289794921875,
- "y": 515.260009765625
+ "x": 870.3289794921875,
+ "y": 545.260009765625
},
{
- "x": 718.260986328125,
- "y": 514.4840087890625
+ "x": 808.260986328125,
+ "y": 544.4840087890625
},
{
- "x": 656.1939697265625,
- "y": 513.7080078125
+ "x": 746.1939697265625,
+ "y": 543.7080078125
},
{
- "x": 598.0040283203125,
- "y": 484.1579895019531
+ "x": 688.0040283203125,
+ "y": 514.1580200195312
},
{
- "x": 560.760986328125,
- "y": 434.5
+ "x": 650.760986328125,
+ "y": 464.5
}
],
"isCurve": true,
@@ -1104,32 +1104,32 @@
"link": "",
"route": [
{
- "x": 535.760986328125,
- "y": 390.5
+ "x": 625.760986328125,
+ "y": 420.5
},
{
- "x": 513,
- "y": 335.0880126953125
+ "x": 603,
+ "y": 365.0880126953125
},
{
- "x": 516.27197265625,
- "y": 272.3909912109375
+ "x": 606.27197265625,
+ "y": 302.3909912109375
},
{
- "x": 544.6820068359375,
- "y": 219.6510009765625
+ "x": 634.6820068359375,
+ "y": 249.6510009765625
},
{
- "x": 573.0910034179688,
- "y": 166.91099548339844
+ "x": 663.0910034179688,
+ "y": 196.91099548339844
},
{
- "x": 623.64501953125,
- "y": 129.68299865722656
+ "x": 713.64501953125,
+ "y": 159.68299865722656
},
{
- "x": 682.4390258789062,
- "y": 118.20600128173828
+ "x": 772.4390258789062,
+ "y": 148.20599365234375
}
],
"isCurve": true,
@@ -1164,32 +1164,32 @@
"link": "",
"route": [
{
- "x": 907.760986328125,
- "y": 384.5
+ "x": 997.760986328125,
+ "y": 414.5
},
{
- "x": 928.6810302734375,
- "y": 329.5469970703125
+ "x": 1018.6810302734375,
+ "y": 359.5469970703125
},
{
- "x": 924.0180053710938,
- "y": 268.1990051269531
+ "x": 1014.0180053710938,
+ "y": 298.1990051269531
},
{
- "x": 895.3189697265625,
- "y": 216.8800048828125
+ "x": 985.3189697265625,
+ "y": 246.8800048828125
},
{
- "x": 866.6190185546875,
- "y": 165.56100463867188
+ "x": 956.6190185546875,
+ "y": 195.56100463867188
},
{
- "x": 816.791015625,
- "y": 129.4709930419922
+ "x": 906.791015625,
+ "y": 159.4709930419922
},
{
- "x": 759.0819702148438,
- "y": 118.20600128173828
+ "x": 849.0819702148438,
+ "y": 148.20599365234375
}
],
"isCurve": true,
@@ -1224,20 +1224,20 @@
"link": "",
"route": [
{
- "x": 1242.9300537109375,
- "y": 96
+ "x": 1392.9300537109375,
+ "y": 126
},
{
- "x": 1217.3299560546875,
- "y": 136
+ "x": 1367.3299560546875,
+ "y": 166
},
{
- "x": 1210.9300537109375,
- "y": 156
+ "x": 1360.9300537109375,
+ "y": 186
},
{
- "x": 1210.9300537109375,
- "y": 196
+ "x": 1360.9300537109375,
+ "y": 226
}
],
"isCurve": true,
@@ -1272,20 +1272,20 @@
"link": "",
"route": [
{
- "x": 1277.9300537109375,
- "y": 96
+ "x": 1427.9300537109375,
+ "y": 126
},
{
- "x": 1387.4300537109375,
- "y": 523.9229736328125
+ "x": 1537.4300537109375,
+ "y": 553.9229736328125
},
{
- "x": 1406.030029296875,
- "y": 545.9229736328125
+ "x": 1556.030029296875,
+ "y": 575.9229736328125
},
{
- "x": 1402.6209716796875,
- "y": 455.8840026855469
+ "x": 1552.6209716796875,
+ "y": 485.8840026855469
}
],
"isCurve": true,
@@ -1320,32 +1320,32 @@
"link": "",
"route": [
{
- "x": 1406.322998046875,
- "y": 511.8840026855469
+ "x": 1556.322998046875,
+ "y": 541.8839721679688
},
{
- "x": 1363.9429931640625,
- "y": 561.60400390625
+ "x": 1513.9429931640625,
+ "y": 591.60400390625
},
{
- "x": 1301.8089599609375,
- "y": 590.1240234375
+ "x": 1451.8089599609375,
+ "y": 620.1240234375
},
{
- "x": 1236.47900390625,
- "y": 589.843994140625
+ "x": 1386.47900390625,
+ "y": 619.843994140625
},
{
- "x": 1171.14794921875,
- "y": 589.5640258789062
+ "x": 1321.14794921875,
+ "y": 619.5640258789062
},
{
- "x": 1109.260986328125,
- "y": 560.5120239257812
+ "x": 1259.260986328125,
+ "y": 590.5120239257812
},
{
- "x": 1067.31005859375,
- "y": 510.42999267578125
+ "x": 1217.31005859375,
+ "y": 540.4299926757812
}
],
"isCurve": true,
@@ -1380,20 +1380,20 @@
"link": "",
"route": [
{
- "x": 1028.9549560546875,
- "y": 443.99798583984375
+ "x": 1178.9549560546875,
+ "y": 473.99798583984375
},
{
- "x": 988,
- "y": 331.76800537109375
+ "x": 1138,
+ "y": 361.76800537109375
},
{
- "x": 1043.239990234375,
- "y": 207.25799560546875
+ "x": 1193.239990234375,
+ "y": 237.25799560546875
},
{
- "x": 1153.9300537109375,
- "y": 162.3070068359375
+ "x": 1303.9300537109375,
+ "y": 192.3070068359375
}
],
"isCurve": true,
@@ -1428,32 +1428,32 @@
"link": "",
"route": [
{
- "x": 1742.5,
- "y": 96.76300048828125
+ "x": 1952.5,
+ "y": 126.76300048828125
},
{
- "x": 1841.8310546875,
- "y": 110.04100036621094
+ "x": 2051.8310546875,
+ "y": 140.04100036621094
},
{
- "x": 1916,
- "y": 194.78500366210938
+ "x": 2126,
+ "y": 224.78500366210938
},
{
- "x": 1916,
- "y": 295
+ "x": 2126,
+ "y": 325
},
{
- "x": 1916,
- "y": 395.2139892578125
+ "x": 2126,
+ "y": 425.2139892578125
},
{
- "x": 1841.8310546875,
- "y": 479.9580078125
+ "x": 2051.8310546875,
+ "y": 509.9580078125
},
{
- "x": 1742.5,
- "y": 493.2359924316406
+ "x": 1952.5,
+ "y": 523.2360229492188
}
],
"isCurve": true,
@@ -1488,32 +1488,32 @@
"link": "",
"route": [
{
- "x": 1689.4990234375,
- "y": 493.2359924316406
+ "x": 1899.4990234375,
+ "y": 523.2360229492188
},
{
- "x": 1590.16796875,
- "y": 479.9580078125
+ "x": 1800.16796875,
+ "y": 509.9580078125
},
{
- "x": 1516,
- "y": 395.2139892578125
+ "x": 1726,
+ "y": 425.2139892578125
},
{
- "x": 1516,
- "y": 295
+ "x": 1726,
+ "y": 325
},
{
- "x": 1516,
- "y": 194.78500366210938
+ "x": 1726,
+ "y": 224.78500366210938
},
{
- "x": 1590.16796875,
- "y": 110.04100036621094
+ "x": 1800.16796875,
+ "y": 140.04100036621094
},
{
- "x": 1689.4990234375,
- "y": 96.76300048828125
+ "x": 1899.4990234375,
+ "y": 126.76300048828125
}
],
"isCurve": true,
diff --git a/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
index a521f90259..83da95a92f 100644
--- a/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
+++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg
@@ -1,16 +1,16 @@
-abcdabcxyzabpq
-
+ .d2-795383471 .fill-N1{fill:#0A0F25;}
+ .d2-795383471 .fill-N4{fill:#CFD2DD;}
+ .d2-795383471 .fill-N5{fill:#DEE1EB;}
+ .d2-795383471 .fill-N7{fill:#FFFFFF;}
+ .d2-795383471 .fill-B1{fill:#0D32B2;}
+ .d2-795383471 .fill-B4{fill:#E3E9FD;}
+ .d2-795383471 .fill-B5{fill:#EDF0FD;}
+ .d2-795383471 .fill-B6{fill:#F7F8FE;}
+ .d2-795383471 .stroke-B1{stroke:#0D32B2;}
+ .d2-795383471 .stroke-B2{stroke:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>abcdabcxyzabpq
+
\ 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
index ddd043e73a..bccfe47cc3 100644
--- a/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
@@ -19,8 +19,8 @@
"x": 12,
"y": 99
},
- "width": 454,
- "height": 466,
+ "width": 514,
+ "height": 526,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -57,8 +57,8 @@
"id": "four.a",
"type": "rectangle",
"pos": {
- "x": 212,
- "y": 99
+ "x": 242,
+ "y": 129
},
"width": 53,
"height": 66,
@@ -99,8 +99,8 @@
"id": "four.b",
"type": "rectangle",
"pos": {
- "x": 412,
- "y": 299
+ "x": 442,
+ "y": 329
},
"width": 53,
"height": 66,
@@ -141,8 +141,8 @@
"id": "four.c",
"type": "rectangle",
"pos": {
- "x": 212,
- "y": 499
+ "x": 242,
+ "y": 529
},
"width": 53,
"height": 66,
@@ -183,8 +183,8 @@
"id": "four.d",
"type": "rectangle",
"pos": {
- "x": 12,
- "y": 299
+ "x": 42,
+ "y": 329
},
"width": 54,
"height": 66,
@@ -225,11 +225,11 @@
"id": "nonrect",
"type": "cycle",
"pos": {
- "x": 485,
+ "x": 545,
"y": 111
},
- "width": 421,
- "height": 441,
+ "width": 481,
+ "height": 501,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -266,8 +266,8 @@
"id": "nonrect.a",
"type": "oval",
"pos": {
- "x": 653,
- "y": 111
+ "x": 743,
+ "y": 141
},
"width": 80,
"height": 77,
@@ -308,8 +308,8 @@
"id": "nonrect.b",
"type": "hexagon",
"pos": {
- "x": 826,
- "y": 415
+ "x": 916,
+ "y": 445
},
"width": 80,
"height": 69,
@@ -350,8 +350,8 @@
"id": "nonrect.c",
"type": "diamond",
"pos": {
- "x": 496,
- "y": 404
+ "x": 586,
+ "y": 434
},
"width": 46,
"height": 92,
@@ -392,11 +392,11 @@
"id": "mixed",
"type": "cycle",
"pos": {
- "x": 926,
+ "x": 1046,
"y": 12
},
- "width": 509,
- "height": 641,
+ "width": 569,
+ "height": 701,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -433,8 +433,8 @@
"id": "mixed.x",
"type": "rectangle",
"pos": {
- "x": 1097,
- "y": 12
+ "x": 1247,
+ "y": 42
},
"width": 201,
"height": 312,
@@ -475,8 +475,8 @@
"id": "mixed.x.p",
"type": "rectangle",
"pos": {
- "x": 1167,
- "y": 62
+ "x": 1317,
+ "y": 92
},
"width": 80,
"height": 66,
@@ -517,8 +517,8 @@
"id": "mixed.x.q",
"type": "rectangle",
"pos": {
- "x": 1147,
- "y": 208
+ "x": 1297,
+ "y": 238
},
"width": 54,
"height": 66,
@@ -559,8 +559,8 @@
"id": "mixed.y",
"type": "rectangle",
"pos": {
- "x": 1380,
- "y": 497
+ "x": 1530,
+ "y": 527
},
"width": 54,
"height": 66,
@@ -601,8 +601,8 @@
"id": "mixed.z",
"type": "oval",
"pos": {
- "x": 949,
- "y": 492
+ "x": 1099,
+ "y": 522
},
"width": 77,
"height": 77,
@@ -643,11 +643,11 @@
"id": "two",
"type": "cycle",
"pos": {
- "x": 1454,
+ "x": 1634,
"y": 99
},
- "width": 400,
- "height": 466,
+ "width": 460,
+ "height": 526,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -684,8 +684,8 @@
"id": "two.a",
"type": "rectangle",
"pos": {
- "x": 1628,
- "y": 99
+ "x": 1838,
+ "y": 129
},
"width": 53,
"height": 66,
@@ -726,8 +726,8 @@
"id": "two.b",
"type": "rectangle",
"pos": {
- "x": 1628,
- "y": 499
+ "x": 1838,
+ "y": 529
},
"width": 53,
"height": 66,
@@ -792,20 +792,20 @@
"link": "",
"route": [
{
- "x": 265.5,
- "y": 133.8780059814453
+ "x": 295.5,
+ "y": 163.8780059814453
},
{
- "x": 352.5150146484375,
- "y": 145.50999450683594
+ "x": 382.5150146484375,
+ "y": 175.50999450683594
},
{
- "x": 421.77301025390625,
- "y": 212.5279998779297
+ "x": 451.77301025390625,
+ "y": 242.5279998779297
},
{
- "x": 436.25799560546875,
- "y": 299.114990234375
+ "x": 466.25799560546875,
+ "y": 329.114990234375
}
],
"isCurve": true,
@@ -840,20 +840,20 @@
"link": "",
"route": [
{
- "x": 436.25799560546875,
- "y": 365.114990234375
+ "x": 466.25799560546875,
+ "y": 395.114990234375
},
{
- "x": 421.77301025390625,
- "y": 451.70098876953125
+ "x": 451.77301025390625,
+ "y": 481.70098876953125
},
{
- "x": 352.5150146484375,
- "y": 518.718994140625
+ "x": 382.5150146484375,
+ "y": 548.718994140625
},
{
- "x": 265.5,
- "y": 530.3510131835938
+ "x": 295.5,
+ "y": 560.3510131835938
}
],
"isCurve": true,
@@ -888,20 +888,20 @@
"link": "",
"route": [
{
- "x": 212.49899291992188,
- "y": 530.3510131835938
+ "x": 242.49899291992188,
+ "y": 560.3510131835938
},
{
- "x": 125.48400115966797,
- "y": 518.718994140625
+ "x": 155.48399353027344,
+ "y": 548.718994140625
},
{
- "x": 56.22600173950195,
- "y": 451.70098876953125
+ "x": 86.22599792480469,
+ "y": 481.70098876953125
},
{
- "x": 41.74100112915039,
- "y": 365.114990234375
+ "x": 71.74099731445312,
+ "y": 395.114990234375
}
],
"isCurve": true,
@@ -936,20 +936,20 @@
"link": "",
"route": [
{
- "x": 41.74100112915039,
- "y": 299.114990234375
+ "x": 71.74099731445312,
+ "y": 329.114990234375
},
{
- "x": 56.22600173950195,
- "y": 212.5279998779297
+ "x": 86.22599792480469,
+ "y": 242.5279998779297
},
{
- "x": 125.48400115966797,
- "y": 145.50999450683594
+ "x": 155.48399353027344,
+ "y": 175.50999450683594
},
{
- "x": 212.49899291992188,
- "y": 133.8780059814453
+ "x": 242.49899291992188,
+ "y": 163.8780059814453
}
],
"isCurve": true,
@@ -984,32 +984,32 @@
"link": "",
"route": [
{
- "x": 732.9439697265625,
- "y": 154.35800170898438
+ "x": 822.9439697265625,
+ "y": 184.35800170898438
},
{
- "x": 789.5540161132812,
- "y": 165.84100341796875
+ "x": 879.5540161132812,
+ "y": 195.84100341796875
},
{
- "x": 838.4299926757812,
- "y": 201.24400329589844
+ "x": 928.4299926757812,
+ "y": 231.24400329589844
},
{
- "x": 866.9940185546875,
- "y": 251.44900512695312
+ "x": 956.9940185546875,
+ "y": 281.4490051269531
},
{
- "x": 895.5590209960938,
- "y": 301.65399169921875
+ "x": 985.5590209960938,
+ "y": 331.65399169921875
},
{
- "x": 901.0230102539062,
- "y": 361.7569885253906
+ "x": 991.0230102539062,
+ "y": 391.7569885253906
},
{
- "x": 882.1610107421875,
- "y": 416.35400390625
+ "x": 972.1610107421875,
+ "y": 446.35400390625
}
],
"isCurve": true,
@@ -1044,32 +1044,32 @@
"link": "",
"route": [
{
- "x": 844.1610107421875,
- "y": 481.35400390625
+ "x": 934.1610107421875,
+ "y": 511.35400390625
},
{
- "x": 804.6160278320312,
- "y": 527.0809936523438
+ "x": 894.6160278320312,
+ "y": 557.0809936523438
},
{
- "x": 746.469970703125,
- "y": 552.3759765625
+ "x": 836.469970703125,
+ "y": 582.3759765625
},
{
- "x": 686.0540161132812,
- "y": 550.2269897460938
+ "x": 776.0540161132812,
+ "y": 580.2269897460938
},
{
- "x": 625.6370239257812,
- "y": 548.0789794921875
+ "x": 715.6370239257812,
+ "y": 578.0789794921875
},
{
- "x": 569.4340209960938,
- "y": 518.718017578125
+ "x": 659.4340209960938,
+ "y": 548.718017578125
},
{
- "x": 533.1610107421875,
- "y": 470.35400390625
+ "x": 623.1610107421875,
+ "y": 500.35400390625
}
],
"isCurve": true,
@@ -1104,32 +1104,32 @@
"link": "",
"route": [
{
- "x": 508.1610107421875,
- "y": 426.35400390625
+ "x": 598.1610107421875,
+ "y": 456.35400390625
},
{
- "x": 485.5,
- "y": 371.1830139160156
+ "x": 575.5,
+ "y": 401.1830139160156
},
{
- "x": 488.6409912109375,
- "y": 308.77801513671875
+ "x": 578.6409912109375,
+ "y": 338.77801513671875
},
{
- "x": 516.72998046875,
- "y": 256.1629943847656
+ "x": 606.72998046875,
+ "y": 286.1629943847656
},
{
- "x": 544.8189697265625,
- "y": 203.54800415039062
+ "x": 634.8189697265625,
+ "y": 233.54800415039062
},
{
- "x": 594.9249877929688,
- "y": 166.21499633789062
+ "x": 684.9249877929688,
+ "y": 196.21499633789062
},
{
- "x": 653.3779907226562,
- "y": 154.35800170898438
+ "x": 743.3779907226562,
+ "y": 184.35800170898438
}
],
"isCurve": true,
@@ -1164,32 +1164,32 @@
"link": "",
"route": [
{
- "x": 882.1610107421875,
- "y": 416.35400390625
+ "x": 972.1610107421875,
+ "y": 446.35400390625
},
{
- "x": 901.0230102539062,
- "y": 361.7569885253906
+ "x": 991.0230102539062,
+ "y": 391.7569885253906
},
{
- "x": 895.5590209960938,
- "y": 301.65399169921875
+ "x": 985.5590209960938,
+ "y": 331.65399169921875
},
{
- "x": 866.9940185546875,
- "y": 251.44900512695312
+ "x": 956.9940185546875,
+ "y": 281.4490051269531
},
{
- "x": 838.4299926757812,
- "y": 201.24400329589844
+ "x": 928.4299926757812,
+ "y": 231.24400329589844
},
{
- "x": 789.5540161132812,
- "y": 165.84100341796875
+ "x": 879.5540161132812,
+ "y": 195.84100341796875
},
{
- "x": 732.9439697265625,
- "y": 154.35800170898438
+ "x": 822.9439697265625,
+ "y": 184.35800170898438
}
],
"isCurve": true,
@@ -1224,20 +1224,20 @@
"link": "",
"route": [
{
- "x": 1194.623046875,
- "y": 128
+ "x": 1344.623046875,
+ "y": 158
},
{
- "x": 1194.623046875,
- "y": 168
+ "x": 1344.623046875,
+ "y": 198
},
{
- "x": 1174.4560546875,
- "y": 168
+ "x": 1324.4560546875,
+ "y": 198
},
{
- "x": 1174.4560546875,
- "y": 208
+ "x": 1324.4560546875,
+ "y": 238
}
],
"animated": false,
@@ -1271,20 +1271,20 @@
"link": "",
"route": [
{
- "x": 1221.2889404296875,
- "y": 128
+ "x": 1371.2889404296875,
+ "y": 158
},
{
- "x": 1333.7889404296875,
- "y": 617.989990234375
+ "x": 1483.7889404296875,
+ "y": 647.989990234375
},
{
- "x": 1353.9560546875,
- "y": 617.989990234375
+ "x": 1503.9560546875,
+ "y": 647.989990234375
},
{
- "x": 1407.5260009765625,
- "y": 497.9859924316406
+ "x": 1557.5260009765625,
+ "y": 527.9860229492188
}
],
"animated": false,
@@ -1318,32 +1318,32 @@
"link": "",
"route": [
{
- "x": 1384.6240234375,
- "y": 563.9860229492188
+ "x": 1534.6240234375,
+ "y": 593.9860229492188
},
{
- "x": 1338.468017578125,
- "y": 619.9349975585938
+ "x": 1488.468017578125,
+ "y": 649.9349975585938
},
{
- "x": 1269.6490478515625,
- "y": 652.22998046875
+ "x": 1419.6490478515625,
+ "y": 682.22998046875
},
{
- "x": 1197.1190185546875,
- "y": 651.97900390625
+ "x": 1347.1190185546875,
+ "y": 681.97900390625
},
{
- "x": 1124.5889892578125,
- "y": 651.72900390625
+ "x": 1274.5889892578125,
+ "y": 681.72900390625
},
{
- "x": 1055.9959716796875,
- "y": 618.9580078125
+ "x": 1205.9959716796875,
+ "y": 648.9580078125
},
{
- "x": 1010.2310180664062,
- "y": 562.68798828125
+ "x": 1160.23095703125,
+ "y": 592.68798828125
}
],
"isCurve": true,
@@ -1378,20 +1378,20 @@
"link": "",
"route": [
{
- "x": 971.85302734375,
- "y": 496.21600341796875
+ "x": 1121.85302734375,
+ "y": 526.2160034179688
},
{
- "x": 926.3660278320312,
- "y": 376.9519958496094
+ "x": 1076.365966796875,
+ "y": 406.9519958496094
},
{
- "x": 981.3410034179688,
- "y": 242.86700439453125
+ "x": 1131.3409423828125,
+ "y": 272.86700439453125
},
{
- "x": 1097.4560546875,
- "y": 189.8560028076172
+ "x": 1247.4560546875,
+ "y": 219.8560028076172
}
],
"isCurve": true,
@@ -1426,32 +1426,32 @@
"link": "",
"route": [
{
- "x": 1681.0260009765625,
- "y": 133.8780059814453
+ "x": 1891.0260009765625,
+ "y": 163.8780059814453
},
{
- "x": 1780.3570556640625,
- "y": 147.15699768066406
+ "x": 1990.3570556640625,
+ "y": 177.15699768066406
},
{
- "x": 1854.5260009765625,
- "y": 231.89999389648438
+ "x": 2064.52587890625,
+ "y": 261.8999938964844
},
{
- "x": 1854.5260009765625,
- "y": 332.114990234375
+ "x": 2064.52587890625,
+ "y": 362.114990234375
},
{
- "x": 1854.5260009765625,
- "y": 432.3299865722656
+ "x": 2064.52587890625,
+ "y": 462.3299865722656
},
{
- "x": 1780.3570556640625,
- "y": 517.072998046875
+ "x": 1990.3570556640625,
+ "y": 547.072998046875
},
{
- "x": 1681.0260009765625,
- "y": 530.3510131835938
+ "x": 1891.0260009765625,
+ "y": 560.3510131835938
}
],
"isCurve": true,
@@ -1486,32 +1486,32 @@
"link": "",
"route": [
{
- "x": 1628.0260009765625,
- "y": 530.3510131835938
+ "x": 1838.0260009765625,
+ "y": 560.3510131835938
},
{
- "x": 1528.6949462890625,
- "y": 517.072998046875
+ "x": 1738.6949462890625,
+ "y": 547.072998046875
},
{
- "x": 1454.5260009765625,
- "y": 432.3299865722656
+ "x": 1664.5260009765625,
+ "y": 462.3299865722656
},
{
- "x": 1454.5260009765625,
- "y": 332.114990234375
+ "x": 1664.5260009765625,
+ "y": 362.114990234375
},
{
- "x": 1454.5260009765625,
- "y": 231.89999389648438
+ "x": 1664.5260009765625,
+ "y": 261.8999938964844
},
{
- "x": 1528.6949462890625,
- "y": 147.15699768066406
+ "x": 1738.6949462890625,
+ "y": 177.15699768066406
},
{
- "x": 1628.0260009765625,
- "y": 133.8780059814453
+ "x": 1838.0260009765625,
+ "y": 163.8780059814453
}
],
"isCurve": true,
diff --git a/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
index 0502fbe56d..d7adc6a814 100644
--- a/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
+++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg
@@ -1,16 +1,16 @@
-abcdabcxyzabpq
-
+ .d2-700540364 .fill-N1{fill:#0A0F25;}
+ .d2-700540364 .fill-N4{fill:#CFD2DD;}
+ .d2-700540364 .fill-N5{fill:#DEE1EB;}
+ .d2-700540364 .fill-N7{fill:#FFFFFF;}
+ .d2-700540364 .fill-B1{fill:#0D32B2;}
+ .d2-700540364 .fill-B4{fill:#E3E9FD;}
+ .d2-700540364 .fill-B5{fill:#EDF0FD;}
+ .d2-700540364 .fill-B6{fill:#F7F8FE;}
+ .d2-700540364 .stroke-B1{stroke:#0D32B2;}
+ .d2-700540364 .stroke-B2{stroke:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>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
index 52d9ba2a41..813ddcfcfe 100644
--- a/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json
@@ -19,8 +19,8 @@
"x": 0,
"y": 0
},
- "width": 453,
- "height": 466,
+ "width": 513,
+ "height": 526,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -57,8 +57,8 @@
"id": "four.a",
"type": "rectangle",
"pos": {
- "x": 200,
- "y": 0
+ "x": 230,
+ "y": 30
},
"width": 53,
"height": 66,
@@ -99,8 +99,8 @@
"id": "four.b",
"type": "rectangle",
"pos": {
- "x": 400,
- "y": 200
+ "x": 430,
+ "y": 230
},
"width": 53,
"height": 66,
@@ -141,8 +141,8 @@
"id": "four.c",
"type": "rectangle",
"pos": {
- "x": 200,
- "y": 400
+ "x": 230,
+ "y": 430
},
"width": 53,
"height": 66,
@@ -183,8 +183,8 @@
"id": "four.d",
"type": "rectangle",
"pos": {
- "x": 0,
- "y": 200
+ "x": 30,
+ "y": 230
},
"width": 54,
"height": 66,
@@ -225,11 +225,11 @@
"id": "nonrect",
"type": "cycle",
"pos": {
- "x": 473,
+ "x": 533,
"y": 0
},
- "width": 440,
- "height": 464,
+ "width": 500,
+ "height": 524,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -266,8 +266,8 @@
"id": "nonrect.a",
"type": "oval",
"pos": {
- "x": 619,
- "y": 0
+ "x": 709,
+ "y": 30
},
"width": 120,
"height": 120,
@@ -308,8 +308,8 @@
"id": "nonrect.b",
"type": "hexagon",
"pos": {
- "x": 792,
- "y": 299
+ "x": 882,
+ "y": 329
},
"width": 121,
"height": 121,
@@ -350,8 +350,8 @@
"id": "nonrect.c",
"type": "diamond",
"pos": {
- "x": 483,
- "y": 314
+ "x": 573,
+ "y": 344
},
"width": 46,
"height": 92,
@@ -393,10 +393,10 @@
"type": "cycle",
"pos": {
"x": 0,
- "y": 486
+ "y": 546
},
- "width": 499,
- "height": 633,
+ "width": 559,
+ "height": 693,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -433,8 +433,8 @@
"id": "mixed.x",
"type": "rectangle",
"pos": {
- "x": 180,
- "y": 486
+ "x": 210,
+ "y": 576
},
"width": 174,
"height": 318,
@@ -475,8 +475,8 @@
"id": "mixed.x.p",
"type": "rectangle",
"pos": {
- "x": 241,
- "y": 546
+ "x": 271,
+ "y": 636
},
"width": 53,
"height": 66,
@@ -517,8 +517,8 @@
"id": "mixed.x.q",
"type": "rectangle",
"pos": {
- "x": 240,
- "y": 678
+ "x": 270,
+ "y": 768
},
"width": 54,
"height": 66,
@@ -559,8 +559,8 @@
"id": "mixed.y",
"type": "rectangle",
"pos": {
- "x": 445,
- "y": 967
+ "x": 475,
+ "y": 1057
},
"width": 54,
"height": 66,
@@ -601,8 +601,8 @@
"id": "mixed.z",
"type": "oval",
"pos": {
- "x": 23,
- "y": 961
+ "x": 53,
+ "y": 1051
},
"width": 77,
"height": 77,
@@ -643,11 +643,11 @@
"id": "two",
"type": "cycle",
"pos": {
- "x": 520,
- "y": 486
+ "x": 580,
+ "y": 546
},
- "width": 400,
- "height": 520,
+ "width": 460,
+ "height": 580,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -684,8 +684,8 @@
"id": "two.a",
"type": "rectangle",
"pos": {
- "x": 660,
- "y": 486
+ "x": 750,
+ "y": 576
},
"width": 120,
"height": 120,
@@ -726,8 +726,8 @@
"id": "two.b",
"type": "rectangle",
"pos": {
- "x": 660,
- "y": 886
+ "x": 750,
+ "y": 976
},
"width": 120,
"height": 120,
@@ -792,20 +792,20 @@
"link": "",
"route": [
{
- "x": 253.5,
- "y": 34.76300048828125
+ "x": 283.5,
+ "y": 64.76300048828125
},
{
- "x": 340.5150146484375,
- "y": 46.39500045776367
+ "x": 370.5150146484375,
+ "y": 76.3949966430664
},
{
- "x": 409.77301025390625,
- "y": 113.41300201416016
+ "x": 439.77301025390625,
+ "y": 143.41299438476562
},
{
- "x": 424.25799560546875,
- "y": 199.99899291992188
+ "x": 454.25799560546875,
+ "y": 229.99899291992188
}
],
"isCurve": true,
@@ -840,20 +840,20 @@
"link": "",
"route": [
{
- "x": 424.25799560546875,
- "y": 266
+ "x": 454.25799560546875,
+ "y": 296
},
{
- "x": 409.77301025390625,
- "y": 352.58599853515625
+ "x": 439.77301025390625,
+ "y": 382.58599853515625
},
{
- "x": 340.5150146484375,
- "y": 419.60400390625
+ "x": 370.5150146484375,
+ "y": 449.60400390625
},
{
- "x": 253.5,
- "y": 431.2359924316406
+ "x": 283.5,
+ "y": 461.2359924316406
}
],
"isCurve": true,
@@ -888,20 +888,20 @@
"link": "",
"route": [
{
- "x": 200.49899291992188,
- "y": 431.2359924316406
+ "x": 230.49899291992188,
+ "y": 461.2359924316406
},
{
- "x": 113.48400115966797,
- "y": 419.60400390625
+ "x": 143.48399353027344,
+ "y": 449.60400390625
},
{
- "x": 44.22600173950195,
- "y": 352.58599853515625
+ "x": 74.22599792480469,
+ "y": 382.58599853515625
},
{
- "x": 29.740999221801758,
- "y": 266
+ "x": 59.74100112915039,
+ "y": 296
}
],
"isCurve": true,
@@ -936,20 +936,20 @@
"link": "",
"route": [
{
- "x": 29.740999221801758,
- "y": 199.99899291992188
+ "x": 59.74100112915039,
+ "y": 229.99899291992188
},
{
- "x": 44.22600173950195,
- "y": 113.41300201416016
+ "x": 74.22599792480469,
+ "y": 143.41299438476562
},
{
- "x": 113.48400115966797,
- "y": 46.39500045776367
+ "x": 143.48399353027344,
+ "y": 76.3949966430664
},
{
- "x": 200.49899291992188,
- "y": 34.76300048828125
+ "x": 230.49899291992188,
+ "y": 64.76300048828125
}
],
"isCurve": true,
@@ -984,20 +984,20 @@
"link": "",
"route": [
{
- "x": 739.1439819335938,
- "y": 69.00499725341797
+ "x": 829.1439819335938,
+ "y": 99.00499725341797
},
{
- "x": 837.260009765625,
- "y": 99.4729995727539
+ "x": 927.260009765625,
+ "y": 129.47300720214844
},
{
- "x": 896.3280029296875,
- "y": 199.3280029296875
+ "x": 986.3280029296875,
+ "y": 229.3280029296875
},
{
- "x": 875.822998046875,
- "y": 300
+ "x": 965.822998046875,
+ "y": 330
}
],
"isCurve": true,
@@ -1032,32 +1032,32 @@
"link": "",
"route": [
{
- "x": 815.822998046875,
- "y": 407
+ "x": 905.822998046875,
+ "y": 437
},
{
- "x": 774.0809936523438,
- "y": 445.260009765625
+ "x": 864.0809936523438,
+ "y": 475.260009765625
},
{
- "x": 718.166015625,
- "y": 464.29400634765625
+ "x": 808.166015625,
+ "y": 494.29400634765625
},
{
- "x": 661.7730102539062,
- "y": 459.1830139160156
+ "x": 751.7730102539062,
+ "y": 489.1830139160156
},
{
- "x": 605.3809814453125,
- "y": 454.072998046875
+ "x": 695.3809814453125,
+ "y": 484.072998046875
},
{
- "x": 553.7969970703125,
- "y": 425.2980041503906
+ "x": 643.7969970703125,
+ "y": 455.2980041503906
},
{
- "x": 519.822998046875,
- "y": 380
+ "x": 609.822998046875,
+ "y": 410
}
],
"isCurve": true,
@@ -1092,32 +1092,32 @@
"link": "",
"route": [
{
- "x": 494.822998046875,
- "y": 336
+ "x": 584.822998046875,
+ "y": 366
},
{
- "x": 473.5,
- "y": 284.08599853515625
+ "x": 563.5,
+ "y": 314.08599853515625
},
{
- "x": 474.96600341796875,
- "y": 225.6020050048828
+ "x": 564.9660034179688,
+ "y": 255.6020050048828
},
{
- "x": 498.86700439453125,
- "y": 174.82400512695312
+ "x": 588.8670043945312,
+ "y": 204.82400512695312
},
{
- "x": 522.7680053710938,
- "y": 124.0459976196289
+ "x": 612.7680053710938,
+ "y": 154.04600524902344
},
{
- "x": 566.905029296875,
- "y": 85.64600372314453
+ "x": 656.905029296875,
+ "y": 115.64600372314453
},
{
- "x": 620.5029907226562,
- "y": 69.00499725341797
+ "x": 710.5029907226562,
+ "y": 99.00499725341797
}
],
"isCurve": true,
@@ -1152,20 +1152,20 @@
"link": "",
"route": [
{
- "x": 875.822998046875,
- "y": 300
+ "x": 965.822998046875,
+ "y": 330
},
{
- "x": 896.3280029296875,
- "y": 199.3280029296875
+ "x": 986.3280029296875,
+ "y": 229.3280029296875
},
{
- "x": 837.260009765625,
- "y": 99.4729995727539
+ "x": 927.260009765625,
+ "y": 129.47300720214844
},
{
- "x": 739.1439819335938,
- "y": 69.00499725341797
+ "x": 829.1439819335938,
+ "y": 99.00499725341797
}
],
"isCurve": true,
@@ -1200,12 +1200,12 @@
"link": "",
"route": [
{
- "x": 267.4649963378906,
- "y": 612
+ "x": 297.4649963378906,
+ "y": 702
},
{
- "x": 267.4649963378906,
- "y": 678
+ "x": 297.4649963378906,
+ "y": 768
}
],
"animated": false,
@@ -1239,12 +1239,12 @@
"link": "",
"route": [
{
- "x": 294.4649963378906,
- "y": 579
+ "x": 324.4649963378906,
+ "y": 669
},
{
- "x": 445.7099914550781,
- "y": 1000.4949951171875
+ "x": 475.7099914550781,
+ "y": 1090.4949951171875
}
],
"animated": false,
@@ -1278,32 +1278,32 @@
"link": "",
"route": [
{
- "x": 449.7170104980469,
- "y": 1033.4949951171875
+ "x": 479.7170104980469,
+ "y": 1123.4949951171875
},
{
- "x": 404.49700927734375,
- "y": 1087.89501953125
+ "x": 434.49700927734375,
+ "y": 1177.89501953125
},
{
- "x": 337.34100341796875,
- "y": 1119.25
+ "x": 367.34100341796875,
+ "y": 1209.25
},
{
- "x": 266.60198974609375,
- "y": 1118.9930419921875
+ "x": 296.60198974609375,
+ "y": 1208.9930419921875
},
{
- "x": 195.86199951171875,
- "y": 1118.7349853515625
+ "x": 225.86199951171875,
+ "y": 1208.7349853515625
},
{
- "x": 128.93600463867188,
- "y": 1086.8909912109375
+ "x": 158.93600463867188,
+ "y": 1176.8909912109375
},
{
- "x": 84.12000274658203,
- "y": 1032.1590576171875
+ "x": 114.12000274658203,
+ "y": 1122.1590576171875
}
],
"isCurve": true,
@@ -1338,20 +1338,20 @@
"link": "",
"route": [
{
- "x": 45.74800109863281,
- "y": 965.697021484375
+ "x": 75.74800109863281,
+ "y": 1055.697021484375
},
{
- "x": 0,
- "y": 844.5339965820312
+ "x": 30,
+ "y": 934.5339965820312
},
{
- "x": 59.994998931884766,
- "y": 709.0880126953125
+ "x": 89.99500274658203,
+ "y": 799.0880126953125
},
{
- "x": 180.46499633789062,
- "y": 661.5460205078125
+ "x": 210.46499633789062,
+ "y": 751.5460205078125
}
],
"isCurve": true,
@@ -1386,32 +1386,32 @@
"link": "",
"route": [
{
- "x": 780,
- "y": 555.2119750976562
+ "x": 870,
+ "y": 645.2119750976562
},
{
- "x": 863.3200073242188,
- "y": 581.4149780273438
+ "x": 953.3200073242188,
+ "y": 671.4149780273438
},
{
- "x": 920,
- "y": 658.656005859375
+ "x": 1010,
+ "y": 748.656005859375
},
{
- "x": 920,
- "y": 746
+ "x": 1010,
+ "y": 836
},
{
- "x": 920,
- "y": 833.343017578125
+ "x": 1010,
+ "y": 923.343017578125
},
{
- "x": 863.3200073242188,
- "y": 910.583984375
+ "x": 953.3200073242188,
+ "y": 1000.583984375
},
{
- "x": 780,
- "y": 936.7869873046875
+ "x": 870,
+ "y": 1026.7869873046875
}
],
"isCurve": true,
@@ -1446,32 +1446,32 @@
"link": "",
"route": [
{
- "x": 659.9990234375,
- "y": 936.7869873046875
+ "x": 749.9990234375,
+ "y": 1026.7869873046875
},
{
- "x": 576.6790161132812,
- "y": 910.583984375
+ "x": 666.6790161132812,
+ "y": 1000.583984375
},
{
- "x": 520,
- "y": 833.343017578125
+ "x": 610,
+ "y": 923.343017578125
},
{
- "x": 520,
- "y": 746
+ "x": 610,
+ "y": 836
},
{
- "x": 520,
- "y": 658.656005859375
+ "x": 610,
+ "y": 748.656005859375
},
{
- "x": 576.6790161132812,
- "y": 581.4149780273438
+ "x": 666.6790161132812,
+ "y": 671.4149780273438
},
{
- "x": 659.9990234375,
- "y": 555.2119750976562
+ "x": 749.9990234375,
+ "y": 645.2119750976562
}
],
"isCurve": true,
diff --git a/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
index d4e4db7928..adac8f79f3 100644
--- a/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
+++ b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg
@@ -1,16 +1,16 @@
-abcdabcxyzabpq
-
+ .d2-2701627539 .fill-N1{fill:#0A0F25;}
+ .d2-2701627539 .fill-N4{fill:#CFD2DD;}
+ .d2-2701627539 .fill-N5{fill:#DEE1EB;}
+ .d2-2701627539 .fill-N7{fill:#FFFFFF;}
+ .d2-2701627539 .fill-B1{fill:#0D32B2;}
+ .d2-2701627539 .fill-B4{fill:#E3E9FD;}
+ .d2-2701627539 .fill-B5{fill:#EDF0FD;}
+ .d2-2701627539 .fill-B6{fill:#F7F8FE;}
+ .d2-2701627539 .stroke-B1{stroke:#0D32B2;}
+ .d2-2701627539 .stroke-B2{stroke:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>abcdabcxyzabpq
+
\ No newline at end of file