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