diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index ffe01dbc1e..d5d5da268d 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,5 +1,7 @@ #### Features ๐Ÿš€ +- d2layouts: add `shape: cycle` for circular diagram layouts with border-trimmed circular arc routing. [#1578](https://github.com/terrastruct/d2/issues/1578) + #### 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..07c351e999 --- /dev/null +++ b/d2layouts/d2cycle/layout.go @@ -0,0 +1,382 @@ +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 = 200. + padding = 24. + maxArcSweep = math.Pi / 2 + epsilon = 1e-6 +) + +type anglePoint struct { + angle float64 + point *geo.Point +} + +// Layout arranges the root children around a circle and routes their root-level +// edges as cubic Bezier arcs trimmed to each shape's visible perimeter. +func Layout(ctx context.Context, g *d2graph.Graph, coreLayout d2graph.LayoutGraph) error { + objects := g.Root.ChildrenArray + if len(objects) == 0 { + return nil + } + + if coreLayout != nil { + if err := coreLayout(ctx, g); err != nil { + return err + } + } + + for _, obj := range g.Objects { + positionLabelsIcons(obj) + } + + radius := calculateRadius(objects) + positionObjects(objects, radius) + + center := geo.NewPoint(0, 0) + for _, edge := range g.Edges { + if isCycleEdge(g, edge) { + routeCircularArc(edge, center) + } + } + normalizeGraph(g) + return nil +} + +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) +} + +func positionObjects(objects []*d2graph.Object, radius float64) { + for i, obj := range objects { + angle := -math.Pi/2 + 2*math.Pi*float64(i)/float64(len(objects)) + center := pointOnCircle(geo.NewPoint(0, 0), radius, angle) + moveObjectAndInternalEdges(obj, center.X-obj.Width/2, center.Y-obj.Height/2) + } +} + +func moveObjectAndInternalEdges(obj *d2graph.Object, x, y float64) { + if obj.TopLeft == nil { + obj.TopLeft = geo.NewPoint(0, 0) + } + + dx := x - obj.TopLeft.X + dy := y - obj.TopLeft.Y + obj.MoveWithDescendants(dx, dy) + + for _, edge := range obj.Graph.Edges { + if edge.Src == nil || edge.Dst == nil || len(edge.Route) == 0 { + continue + } + + srcMoved := edge.Src.IsDescendantOf(obj) + dstMoved := edge.Dst.IsDescendantOf(obj) + switch { + case srcMoved && dstMoved: + edge.Move(dx, dy) + case srcMoved: + edge.Route[0].X += dx + edge.Route[0].Y += dy + case dstMoved: + i := len(edge.Route) - 1 + edge.Route[i].X += dx + edge.Route[i].Y += dy + } + } +} + +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 +} + +func normalizeGraph(g *d2graph.Graph) { + tl := geo.NewPoint(math.Inf(1), math.Inf(1)) + br := geo.NewPoint(math.Inf(-1), math.Inf(-1)) + + for _, obj := range g.Objects { + if obj.TopLeft == nil { + continue + } + tl.X = math.Min(tl.X, obj.TopLeft.X) + tl.Y = math.Min(tl.Y, obj.TopLeft.Y) + br.X = math.Max(br.X, obj.TopLeft.X+obj.Width) + br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height) + } + for _, edge := range g.Edges { + for _, point := range edge.Route { + tl.X = math.Min(tl.X, point.X) + tl.Y = math.Min(tl.Y, point.Y) + br.X = math.Max(br.X, point.X) + br.Y = math.Max(br.Y, point.Y) + } + } + + if math.IsInf(tl.X, 0) || math.IsInf(tl.Y, 0) { + return + } + + dx := -tl.X + dy := -tl.Y + for _, obj := range g.Objects { + if obj.TopLeft != nil { + obj.TopLeft.X += dx + obj.TopLeft.Y += dy + } + } + for _, edge := range g.Edges { + edge.Move(dx, dy) + } + g.Root.Box = geo.NewBox(geo.NewPoint(0, 0), br.X-tl.X, br.Y-tl.Y) +} + +func routeCircularArc(edge *d2graph.Edge, center *geo.Point) { + if edge.Src == nil || edge.Dst == nil { + return + } + + srcCenter := edge.Src.Center() + dstCenter := edge.Dst.Center() + radius := (distance(center, srcCenter) + distance(center, dstCenter)) / 2 + if radius == 0 { + return + } + + startAngle := angleOf(center, srcCenter) + endAngle := advanceAngle(startAngle, angleOf(center, dstCenter)) + if edge.Src == edge.Dst || endAngle-startAngle < epsilon { + endAngle = startAngle + 2*math.Pi + } + + start := findBoundaryAngle(edge.Src, center, radius, startAngle, endAngle, true) + end := findBoundaryAngle(edge.Dst, center, radius, startAngle, endAngle, false) + if start == nil || end == nil || end.angle-start.angle < epsilon { + routeStraight(edge) + return + } + + edge.Route = buildArcRoute(center, radius, start.angle, end.angle) + if len(edge.Route) >= 4 { + edge.Route[0] = start.point + edge.Route[len(edge.Route)-1] = end.point + edge.IsCurve = true + return + } + + routeStraight(edge) +} + +func routeStraight(edge *d2graph.Edge) { + edge.Route = []*geo.Point{edge.Src.Center(), edge.Dst.Center()} + edge.TraceToShape(edge.Route, 0, 1) + edge.IsCurve = false +} + +func findBoundaryAngle(obj *d2graph.Object, center *geo.Point, radius, startAngle, endAngle float64, fromStart bool) *anglePoint { + var selected *anglePoint + for _, point := range circleShapeIntersections(obj.ToShape(), center, radius, startAngle, endAngle) { + angle := advanceAngle(startAngle, angleOf(center, point)) + if angle <= startAngle+epsilon || angle >= endAngle-epsilon { + continue + } + + candidate := &anglePoint{ + angle: angle, + point: truncatePoint(point), + } + if selected == nil || + (fromStart && candidate.angle < selected.angle) || + (!fromStart && candidate.angle > selected.angle) { + selected = candidate + } + } + + if selected != nil { + return selected + } + return fallbackBoundaryAngle(obj, center, radius, startAngle, endAngle, fromStart) +} + +func fallbackBoundaryAngle(obj *d2graph.Object, center *geo.Point, radius, startAngle, endAngle float64, fromStart bool) *anglePoint { + box := obj.Box + if box == nil { + return nil + } + + low := startAngle + high := endAngle + if fromStart { + for i := 0; i < 48; i++ { + mid := (low + high) / 2 + if box.Contains(pointOnCircle(center, radius, mid)) { + low = mid + } else { + high = mid + } + } + angle := high + point := shape.TraceToShapeBorder(obj.ToShape(), pointOnCircle(center, radius, angle), pointOnCircle(center, radius, angle+0.001)) + return &anglePoint{angle: angle, point: truncatePoint(point)} + } + + for i := 0; i < 48; i++ { + mid := (low + high) / 2 + if box.Contains(pointOnCircle(center, radius, mid)) { + high = mid + } else { + low = mid + } + } + angle := low + point := shape.TraceToShapeBorder(obj.ToShape(), pointOnCircle(center, radius, angle), pointOnCircle(center, radius, angle-0.001)) + return &anglePoint{angle: angle, point: truncatePoint(point)} +} + +func buildArcRoute(center *geo.Point, radius, startAngle, endAngle float64) []*geo.Point { + sweep := endAngle - startAngle + segments := int(math.Ceil(sweep / maxArcSweep)) + if segments < 1 { + segments = 1 + } + + route := []*geo.Point{truncatePoint(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) + 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, truncatePoint(c1), truncatePoint(c2), truncatePoint(p3)) + } + return route +} + +func circleShapeIntersections(s shape.Shape, center *geo.Point, radius, startAngle, endAngle float64) []*geo.Point { + sweep := endAngle - startAngle + steps := int(math.Ceil(sweep / (math.Pi / 180))) + if steps < 24 { + steps = 24 + } + + points := make([]*geo.Point, 0) + for i := 0; i < steps; i++ { + a0 := startAngle + sweep*float64(i)/float64(steps) + a1 := startAngle + sweep*float64(i+1)/float64(steps) + circleSegment := geo.Segment{ + Start: pointOnCircle(center, radius, a0), + End: pointOnCircle(center, radius, a1), + } + for _, perimeter := range s.Perimeter() { + for _, point := range perimeter.Intersections(circleSegment) { + if !hasNearbyPoint(points, point) { + points = append(points, point) + } + } + } + } + return points +} + +func hasNearbyPoint(points []*geo.Point, point *geo.Point) bool { + for _, existing := range points { + if distance(existing, point) < 0.001 { + return true + } + } + return false +} + +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)) +} + +func angleOf(center, point *geo.Point) float64 { + return math.Atan2(point.Y-center.Y, point.X-center.X) +} + +func advanceAngle(reference, angle float64) float64 { + for angle <= reference { + angle += 2 * math.Pi + } + return angle +} + +func distance(a, b *geo.Point) float64 { + return math.Hypot(a.X-b.X, a.Y-b.Y) +} + +func truncatePoint(point *geo.Point) *geo.Point { + point = point.Copy() + point.TruncateDecimals() + return point +} + +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..cdc78d5198 --- /dev/null +++ b/d2layouts/d2cycle/layout_test.go @@ -0,0 +1,221 @@ +package d2cycle + +import ( + "context" + "math" + "strings" + "testing" + + "github.com/d2lang/d2/d2compiler" + "github.com/d2lang/d2/d2graph" + "github.com/d2lang/d2/lib/geo" +) + +func TestCalculateRadiusKeepsSingleObjectAtCenter(t *testing.T) { + g, _, err := d2compiler.Compile("", strings.NewReader(` +shape: cycle +a +`), nil) + if err != nil { + t.Fatal(err) + } + setObjectBoxes(g, 100, 100) + + if got := calculateRadius(g.Root.ChildrenArray); got != 0 { + t.Fatalf("expected single-object cycle radius 0, got %.2f", got) + } +} + +func TestCycleLayoutPreservesNestedCoreLayout(t *testing.T) { + g, _, err := d2compiler.Compile("", strings.NewReader(` +shape: cycle +a: { + x + y + x -> y +} +b +a -> b +`), nil) + if err != nil { + t.Fatal(err) + } + setObjectBoxes(g, 100, 100) + + layoutCalled := false + coreLayout := func(_ context.Context, g *d2graph.Graph) error { + layoutCalled = true + for _, obj := range g.Objects { + switch obj.ID { + case "a": + obj.TopLeft = geo.NewPoint(0, 0) + case "b": + obj.TopLeft = geo.NewPoint(300, 0) + case "x": + obj.TopLeft = geo.NewPoint(20, 30) + case "y": + obj.TopLeft = geo.NewPoint(160, 30) + } + } + for _, edge := range g.Edges { + edge.Route = []*geo.Point{edge.Src.Center(), edge.Dst.Center()} + } + return nil + } + + if err := Layout(context.Background(), g, coreLayout); err != nil { + t.Fatal(err) + } + if !layoutCalled { + t.Fatal("expected cycle layout to call the provided core layout") + } + + var x, y *d2graph.Object + var internalEdge, cycleEdge *d2graph.Edge + for _, obj := range g.Objects { + switch obj.ID { + case "x": + x = obj + case "y": + y = obj + } + } + for _, edge := range g.Edges { + switch { + case edge.Src == x && edge.Dst == y: + internalEdge = edge + case edge.Src.Parent == g.Root && edge.Dst.Parent == g.Root: + cycleEdge = edge + } + } + + if x == nil || y == nil || internalEdge == nil || cycleEdge == nil { + t.Fatal("expected nested objects, nested edge, and root-level cycle edge") + } + if internalEdge.IsCurve { + t.Fatal("expected nested internal edge to keep the core layout route") + } + if got := len(internalEdge.Route); got != 2 { + t.Fatalf("expected nested internal edge route to keep 2 points, got %d", got) + } + if !samePoint(internalEdge.Route[0], x.Center()) { + t.Fatalf("expected nested edge start to move with nested source, got %v want %v", internalEdge.Route[0], x.Center()) + } + if !samePoint(internalEdge.Route[1], y.Center()) { + t.Fatalf("expected nested edge end to move with nested destination, got %v want %v", internalEdge.Route[1], y.Center()) + } + if !cycleEdge.IsCurve { + t.Fatal("expected root-level cycle edge to be routed as a curve") + } +} + +func TestCycleLayoutMovesCrossChildDescendantEdges(t *testing.T) { + g, _, err := d2compiler.Compile("", strings.NewReader(` +shape: cycle +a: { x } +b: { y } +a.x -> b.y +`), nil) + if err != nil { + t.Fatal(err) + } + setObjectBoxes(g, 100, 100) + + coreLayout := func(_ context.Context, g *d2graph.Graph) error { + for _, obj := range g.Objects { + switch obj.ID { + case "a": + obj.TopLeft = geo.NewPoint(0, 0) + case "b": + obj.TopLeft = geo.NewPoint(300, 0) + case "x", "y": + obj.TopLeft = geo.NewPoint(20, 30) + } + } + for _, edge := range g.Edges { + edge.Route = []*geo.Point{edge.Src.Center(), edge.Dst.Center()} + } + return nil + } + + if err := Layout(context.Background(), g, coreLayout); err != nil { + t.Fatal(err) + } + + var x, y *d2graph.Object + for _, obj := range g.Objects { + switch obj.ID { + case "x": + x = obj + case "y": + y = obj + } + } + var edge *d2graph.Edge + for _, e := range g.Edges { + if e.Src == x && e.Dst == y { + edge = e + } + } + + if edge == nil { + t.Fatal("expected cross-child descendant edge") + } + if len(edge.Route) != 2 { + t.Fatalf("expected 2-point route, got %d", len(edge.Route)) + } + if !samePoint(edge.Route[0], x.Center()) { + t.Fatalf("expected edge start at %v, got %v", x.Center(), edge.Route[0]) + } + if !samePoint(edge.Route[1], y.Center()) { + t.Fatalf("expected edge end at %v, got %v", y.Center(), edge.Route[1]) + } +} + +func setObjectBoxes(g *d2graph.Graph, width, height float64) { + for _, obj := range g.Objects { + obj.Box = geo.NewBox(geo.NewPoint(0, 0), width, height) + } +} + +func samePoint(a, b *geo.Point) bool { + return math.Abs(a.X-b.X) < 0.001 && math.Abs(a.Y-b.Y) < 0.001 +} + +func TestCycleLayoutSupportsVariousShapes(t *testing.T) { + g, _, err := d2compiler.Compile("", strings.NewReader(` +shape: cycle +a: { shape: circle } +b: { shape: rectangle } +c: { shape: cylinder } +d: { shape: hexagon } +a -> b -> c -> d -> a +`), nil) + if err != nil { + t.Fatal(err) + } + setObjectBoxes(g, 100, 100) + + coreLayout := func(_ context.Context, g *d2graph.Graph) error { + for _, obj := range g.Objects { + obj.TopLeft = geo.NewPoint(0, 0) + } + return nil + } + + if err := Layout(context.Background(), g, coreLayout); err != nil { + t.Fatal(err) + } + + for _, edge := range g.Edges { + if !edge.IsCurve { + t.Fatalf("expected edge %v -> %v to be curve", edge.Src.ID, edge.Dst.ID) + } + if len(edge.Route) < 4 { + t.Fatalf("expected edge %v -> %v route to have at least 4 points, got %d", edge.Src.ID, edge.Dst.ID, len(edge.Route)) + } + if (len(edge.Route)-1)%3 != 0 { + t.Fatalf("expected edge %v -> %v route length 1+3n, got %d", edge.Src.ID, edge.Dst.ID, len(edge.Route)) + } + } +} diff --git a/d2layouts/d2layouts.go b/d2layouts/d2layouts.go index c9c45479df..30f5ca0f84 100644 --- a/d2layouts/d2layouts.go +++ b/d2layouts/d2layouts.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/d2lang/d2/d2graph" + "github.com/d2lang/d2/d2layouts/d2cycle" "github.com/d2lang/d2/d2layouts/d2grid" "github.com/d2lang/d2/d2layouts/d2near" "github.com/d2lang/d2/d2layouts/d2sequence" @@ -20,12 +21,12 @@ import ( type DiagramType string -// a grid diagram at a constant near is const ( DefaultGraphType DiagramType = "" ConstantNearGraph DiagramType = "constant-near" GridDiagram DiagramType = "grid-diagram" SequenceDiagram DiagramType = "sequence-diagram" + CycleDiagram DiagramType = "cycle-diagram" ) type GraphInfo struct { @@ -289,6 +290,12 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co if err != nil { return err } + case CycleDiagram: + log.Debug(ctx, "layout cycle", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString())) + err = d2cycle.Layout(ctx, g, coreLayout) + if err != nil { + return err + } default: log.Debug(ctx, "default layout", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString())) err := coreLayout(ctx, g) @@ -393,6 +400,8 @@ func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) { gi.DiagramType = SequenceDiagram } else if obj.IsGridDiagram() { gi.DiagramType = GridDiagram + } else if obj.IsCycleDiagram() { + gi.DiagramType = CycleDiagram } return gi } diff --git a/d2renderers/d2raster/target_pixel_coverage_test.go b/d2renderers/d2raster/target_pixel_coverage_test.go index 5146d4ad3c..efc2b01751 100644 --- a/d2renderers/d2raster/target_pixel_coverage_test.go +++ b/d2renderers/d2raster/target_pixel_coverage_test.go @@ -56,6 +56,7 @@ func TestTargetShapePixels(t *testing.T) { d2target.ShapeSQLTable, d2target.ShapeImage, d2target.ShapeSequenceDiagram, + d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, } if !reflect.DeepEqual(shapeValues, d2target.Shapes) { diff --git a/d2renderers/d2raster/testdata/target-shapes.exp.png b/d2renderers/d2raster/testdata/target-shapes.exp.png index c7126219f9..85f894820e 100644 Binary files a/d2renderers/d2raster/testdata/target-shapes.exp.png and b/d2renderers/d2raster/testdata/target-shapes.exp.png differ diff --git a/d2renderers/d2scenebuild/shape_effects.go b/d2renderers/d2scenebuild/shape_effects.go index 8d241ec1a1..ec30c3a7d8 100644 --- a/d2renderers/d2scenebuild/shape_effects.go +++ b/d2renderers/d2scenebuild/shape_effects.go @@ -295,7 +295,7 @@ func (b *builder) buildOrdinaryShapeOutline(targetShape d2target.Shape, fill d2s Fill: fill, Stroke: stroke, })) - case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "": + case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "": radius := clampBorderRadius(float64(targetShape.BorderRadius), box.Width, box.Height) nodes = append(nodes, d2scene.NewNode(d2scene.Rect{ Box: box, diff --git a/d2renderers/d2scenebuild/sketch_shapes.go b/d2renderers/d2scenebuild/sketch_shapes.go index 9f1b1f15fe..56accc2ff1 100644 --- a/d2renderers/d2scenebuild/sketch_shapes.go +++ b/d2renderers/d2scenebuild/sketch_shapes.go @@ -127,7 +127,7 @@ func (b *builder) buildSketchOrdinaryShape(targetShape d2target.Shape, idPrefix object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset, drawable, d2scene.Translate(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), targetShape, ) - case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "": + case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "": drawable := newSketchGenerator().Rectangle(0, 0, float64(targetShape.Width), float64(targetShape.Height), options) nodes, err = b.compileSketchShapeDrawable( object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset, @@ -156,7 +156,7 @@ func (b *builder) buildSketchOrdinaryShape(targetShape d2target.Shape, idPrefix RadiusX: float64(targetShape.Width) / 2, RadiusY: float64(targetShape.Height) / 2, }) - case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "": + case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "": overlayPrimitives = append(overlayPrimitives, d2scene.Rect{Box: sketchShapeBox(targetShape)}) default: for _, node := range roughNodes { @@ -578,7 +578,7 @@ func (b *builder) buildSketchOutlineOnly(targetShape d2target.Shape, idPrefix st ), d2scene.Translate(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), targetShape, ) - case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "": + case d2target.ShapeRectangle, d2target.ShapeSquare, d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, "": return b.compileSketchShapeDrawable( object, idPrefix, fillRaw, strokeRaw, strokeWidth, dashes, dashOffset, newSketchGenerator().Rectangle(0, 0, float64(targetShape.Width), float64(targetShape.Height), options), diff --git a/d2renderers/d2scenebuild/sketch_shapes_test.go b/d2renderers/d2scenebuild/sketch_shapes_test.go index 18040f1263..8792075369 100644 --- a/d2renderers/d2scenebuild/sketch_shapes_test.go +++ b/d2renderers/d2scenebuild/sketch_shapes_test.go @@ -28,7 +28,7 @@ func TestSketchOrdinaryShapeFamiliesUseTypedRoughPathsDeterministically(t *testi d2target.ShapeStep, d2target.ShapeCallout, d2target.ShapeStoredData, d2target.ShapePerson, d2target.ShapeC4Person, d2target.ShapeDiamond, d2target.ShapeHexagon, d2target.ShapeCloud, - d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, + d2target.ShapeSequenceDiagram, d2target.ShapeCycleDiagram, d2target.ShapeHierarchy, } for _, shapeType := range shapeTypes { name := shapeType diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 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..00791c9689 --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json @@ -0,0 +1,884 @@ +{ + "name": "", + "config": { + "sketch": false, + "themeID": 0, + "darkThemeID": null, + "pad": null, + "center": null, + "layoutEngine": null + }, + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "monoFontFamily": "SourceCodePro", + "shapes": [ + { + "id": "1", + "type": "cycle", + "pos": { + "x": 0, + "y": 0 + }, + "width": 453, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "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": "1.a", + "type": "rectangle", + "pos": { + "x": 200, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.b", + "type": "rectangle", + "pos": { + "x": 400, + "y": 200 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.c", + "type": "rectangle", + "pos": { + "x": 200, + "y": 400 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.d", + "type": "rectangle", + "pos": { + "x": 0, + "y": 200 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2", + "type": "cycle", + "pos": { + "x": 513, + "y": 17 + }, + "width": 408, + "height": 433, + "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": "2.a", + "type": "rectangle", + "pos": { + "x": 686, + "y": 17 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.b", + "type": "rectangle", + "pos": { + "x": 859, + "y": 317 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.c", + "type": "rectangle", + "pos": { + "x": 513, + "y": 317 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3", + "type": "cycle", + "pos": { + "x": 981, + "y": 0 + }, + "width": 226, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "3.a", + "type": "rectangle", + "pos": { + "x": 981, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3.b", + "type": "rectangle", + "pos": { + "x": 981, + "y": 400 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "1.(a -> b)[0]", + "src": "1.a", + "srcArrow": "none", + "dst": "1.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 253.5, + "y": 34.763999938964844 + }, + { + "x": 340.5150146484375, + "y": 46.39500045776367 + }, + { + "x": 409.77301025390625, + "y": 113.41400146484375 + }, + { + "x": 424.25799560546875, + "y": 200 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(b -> c)[0]", + "src": "1.b", + "srcArrow": "none", + "dst": "1.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 424.25799560546875, + "y": 266 + }, + { + "x": 409.77301025390625, + "y": 352.58599853515625 + }, + { + "x": 340.5150146484375, + "y": 419.60400390625 + }, + { + "x": 253.5, + "y": 431.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(c -> d)[0]", + "src": "1.c", + "srcArrow": "none", + "dst": "1.d", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 200.5, + "y": 431.2359924316406 + }, + { + "x": 113.48500061035156, + "y": 419.60400390625 + }, + { + "x": 44.22700119018555, + "y": 352.58599853515625 + }, + { + "x": 29.740999221801758, + "y": 266 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(a -> b)[0]", + "src": "2.a", + "srcArrow": "none", + "dst": "2.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 739.2050170898438, + "y": 51.763999938964844 + }, + { + "x": 798.9879760742188, + "y": 59.75600051879883 + }, + { + "x": 851.9719848632812, + "y": 94.30000305175781 + }, + { + "x": 883.4019775390625, + "y": 145.7790069580078 + }, + { + "x": 914.8330078125, + "y": 197.2570037841797 + }, + { + "x": 921.35400390625, + "y": 260.16900634765625 + }, + { + "x": 901.1480102539062, + "y": 316.9989929199219 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(b -> c)[0]", + "src": "2.b", + "srcArrow": "none", + "dst": "2.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 862.072998046875, + "y": 383 + }, + { + "x": 824.1229858398438, + "y": 425.6210021972656 + }, + { + "x": 769.7730102539062, + "y": 450 + }, + { + "x": 712.7050170898438, + "y": 450 + }, + { + "x": 655.6370239257812, + "y": 450 + }, + { + "x": 601.2869873046875, + "y": 425.6210021972656 + }, + { + "x": 563.3369750976562, + "y": 383 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "3.(a -> b)[0]", + "src": "3.a", + "srcArrow": "none", + "dst": "3.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 1034, + "y": 34.763999938964844 + }, + { + "x": 1133.3310546875, + "y": 48.04199981689453 + }, + { + "x": 1207.5, + "y": 132.78599548339844 + }, + { + "x": 1207.5, + "y": 233 + }, + { + "x": 1207.5, + "y": 333.2139892578125 + }, + { + "x": 1133.3310546875, + "y": 417.9570007324219 + }, + { + "x": 1034, + "y": 431.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg new file mode 100644 index 0000000000..1b29c5db5d --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg @@ -0,0 +1,30 @@ +abcdabcab + + + \ 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..850c67df62 --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json @@ -0,0 +1,884 @@ +{ + "name": "", + "config": { + "sketch": false, + "themeID": 0, + "darkThemeID": null, + "pad": null, + "center": null, + "layoutEngine": null + }, + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "monoFontFamily": "SourceCodePro", + "shapes": [ + { + "id": "1", + "type": "cycle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 454, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "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": "1.a", + "type": "rectangle", + "pos": { + "x": 212, + "y": 12 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.b", + "type": "rectangle", + "pos": { + "x": 412, + "y": 212 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.c", + "type": "rectangle", + "pos": { + "x": 212, + "y": 412 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.d", + "type": "rectangle", + "pos": { + "x": 12, + "y": 212 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2", + "type": "cycle", + "pos": { + "x": 485, + "y": 28 + }, + "width": 409, + "height": 433, + "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": "2.a", + "type": "rectangle", + "pos": { + "x": 658, + "y": 28 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.b", + "type": "rectangle", + "pos": { + "x": 831, + "y": 328 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.c", + "type": "rectangle", + "pos": { + "x": 485, + "y": 328 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3", + "type": "cycle", + "pos": { + "x": 913, + "y": 12 + }, + "width": 227, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "3.a", + "type": "rectangle", + "pos": { + "x": 913, + "y": 12 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3.b", + "type": "rectangle", + "pos": { + "x": 913, + "y": 412 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "1.(a -> b)[0]", + "src": "1.a", + "srcArrow": "none", + "dst": "1.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 265.5, + "y": 46.763999938964844 + }, + { + "x": 352.5150146484375, + "y": 58.39500045776367 + }, + { + "x": 421.77301025390625, + "y": 125.41400146484375 + }, + { + "x": 436.25799560546875, + "y": 212 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(b -> c)[0]", + "src": "1.b", + "srcArrow": "none", + "dst": "1.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 436.25799560546875, + "y": 278 + }, + { + "x": 421.77301025390625, + "y": 364.58599853515625 + }, + { + "x": 352.5150146484375, + "y": 431.60400390625 + }, + { + "x": 265.5, + "y": 443.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(c -> d)[0]", + "src": "1.c", + "srcArrow": "none", + "dst": "1.d", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 212.5, + "y": 443.2359924316406 + }, + { + "x": 125.48500061035156, + "y": 431.60400390625 + }, + { + "x": 56.22700119018555, + "y": 364.58599853515625 + }, + { + "x": 41.74100112915039, + "y": 278 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(a -> b)[0]", + "src": "2.a", + "srcArrow": "none", + "dst": "2.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 711.7050170898438, + "y": 63.263999938964844 + }, + { + "x": 771.4879760742188, + "y": 71.25599670410156 + }, + { + "x": 824.4719848632812, + "y": 105.80000305175781 + }, + { + "x": 855.9019775390625, + "y": 157.2790069580078 + }, + { + "x": 887.3330078125, + "y": 208.7570037841797 + }, + { + "x": 893.85400390625, + "y": 271.66900634765625 + }, + { + "x": 873.6480102539062, + "y": 328.4989929199219 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(b -> c)[0]", + "src": "2.b", + "srcArrow": "none", + "dst": "2.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 834.572998046875, + "y": 394.5 + }, + { + "x": 796.6229858398438, + "y": 437.1210021972656 + }, + { + "x": 742.2730102539062, + "y": 461.5 + }, + { + "x": 685.2050170898438, + "y": 461.5 + }, + { + "x": 628.1370239257812, + "y": 461.5 + }, + { + "x": 573.7869873046875, + "y": 437.1210021972656 + }, + { + "x": 535.8369750976562, + "y": 394.5 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "3.(a -> b)[0]", + "src": "3.a", + "srcArrow": "none", + "dst": "3.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 966.85400390625, + "y": 46.763999938964844 + }, + { + "x": 1066.18505859375, + "y": 60.04199981689453 + }, + { + "x": 1140.35400390625, + "y": 144.78599548339844 + }, + { + "x": 1140.35400390625, + "y": 245 + }, + { + "x": 1140.35400390625, + "y": 345.2139892578125 + }, + { + "x": 1066.18505859375, + "y": 429.9570007324219 + }, + { + "x": 966.85400390625, + "y": 443.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg new file mode 100644 index 0000000000..9dcf3d7aaa --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg @@ -0,0 +1,30 @@ +abcdabcab + + + \ No newline at end of file diff --git a/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json b/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json new file mode 100644 index 0000000000..70207139e5 --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/tala/board.exp.json @@ -0,0 +1,884 @@ +{ + "name": "", + "config": { + "sketch": false, + "themeID": 0, + "darkThemeID": null, + "pad": null, + "center": null, + "layoutEngine": null + }, + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "monoFontFamily": "SourceCodePro", + "shapes": [ + { + "id": "1", + "type": "cycle", + "pos": { + "x": 0, + "y": 0 + }, + "width": 453, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "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": "1.a", + "type": "rectangle", + "pos": { + "x": 200, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.b", + "type": "rectangle", + "pos": { + "x": 400, + "y": 200 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.c", + "type": "rectangle", + "pos": { + "x": 200, + "y": 400 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "1.d", + "type": "rectangle", + "pos": { + "x": 0, + "y": 200 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2", + "type": "cycle", + "pos": { + "x": 473, + "y": 0 + }, + "width": 408, + "height": 433, + "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": "2.a", + "type": "rectangle", + "pos": { + "x": 646, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.b", + "type": "rectangle", + "pos": { + "x": 819, + "y": 300 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2.c", + "type": "rectangle", + "pos": { + "x": 473, + "y": 300 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3", + "type": "cycle", + "pos": { + "x": 901, + "y": 0 + }, + "width": 226, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "3.a", + "type": "rectangle", + "pos": { + "x": 901, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3.b", + "type": "rectangle", + "pos": { + "x": 901, + "y": 400 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "1.(a -> b)[0]", + "src": "1.a", + "srcArrow": "none", + "dst": "1.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 253.5, + "y": 34.763999938964844 + }, + { + "x": 340.5150146484375, + "y": 46.39500045776367 + }, + { + "x": 409.77301025390625, + "y": 113.41400146484375 + }, + { + "x": 424.25799560546875, + "y": 200 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(b -> c)[0]", + "src": "1.b", + "srcArrow": "none", + "dst": "1.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 424.25799560546875, + "y": 266 + }, + { + "x": 409.77301025390625, + "y": 352.58599853515625 + }, + { + "x": 340.5150146484375, + "y": 419.60400390625 + }, + { + "x": 253.5, + "y": 431.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "1.(c -> d)[0]", + "src": "1.c", + "srcArrow": "none", + "dst": "1.d", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 200.5, + "y": 431.2359924316406 + }, + { + "x": 113.48500061035156, + "y": 419.60400390625 + }, + { + "x": 44.22700119018555, + "y": 352.58599853515625 + }, + { + "x": 29.740999221801758, + "y": 266 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(a -> b)[0]", + "src": "2.a", + "srcArrow": "none", + "dst": "2.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 699.7050170898438, + "y": 34.763999938964844 + }, + { + "x": 759.4879760742188, + "y": 42.75600051879883 + }, + { + "x": 812.4719848632812, + "y": 77.30000305175781 + }, + { + "x": 843.9019775390625, + "y": 128.7790069580078 + }, + { + "x": 875.3330078125, + "y": 180.2570037841797 + }, + { + "x": 881.85400390625, + "y": 243.1699981689453 + }, + { + "x": 861.6480102539062, + "y": 299.9989929199219 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "2.(b -> c)[0]", + "src": "2.b", + "srcArrow": "none", + "dst": "2.c", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 822.572998046875, + "y": 366 + }, + { + "x": 784.6229858398438, + "y": 408.6210021972656 + }, + { + "x": 730.2730102539062, + "y": 433 + }, + { + "x": 673.2050170898438, + "y": 433 + }, + { + "x": 616.1370239257812, + "y": 433 + }, + { + "x": 561.7869873046875, + "y": 408.6210021972656 + }, + { + "x": 523.8369750976562, + "y": 366 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "3.(a -> b)[0]", + "src": "3.a", + "srcArrow": "none", + "dst": "3.b", + "dstArrow": "triangle", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "B1", + "borderRadius": 10, + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N2", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "link": "", + "route": [ + { + "x": 954.85400390625, + "y": 34.763999938964844 + }, + { + "x": 1054.18505859375, + "y": 48.04199981689453 + }, + { + "x": 1128.35400390625, + "y": 132.78599548339844 + }, + { + "x": 1128.35400390625, + "y": 233 + }, + { + "x": 1128.35400390625, + "y": 333.2139892578125 + }, + { + "x": 1054.18505859375, + "y": 417.9570007324219 + }, + { + "x": 954.85400390625, + "y": 431.2359924316406 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ], + "root": { + "id": "", + "type": "", + "pos": { + "x": 0, + "y": 0 + }, + "width": 0, + "height": 0, + "opacity": 0, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "N7", + "stroke": "", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 0 + } +} diff --git a/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg new file mode 100644 index 0000000000..96b9c9a22a --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/tala/sketch.exp.svg @@ -0,0 +1,30 @@ +abcdabcab + + + \ No newline at end of file diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt index 7585d91b3e..be247de7f7 100644 --- a/e2etests/txtar.txt +++ b/e2etests/txtar.txt @@ -1773,3 +1773,17 @@ style: {fill-pattern: dots; fill:"radial-gradient(#fbfbf8, #e3e3f0)"; stroke: "# a->b + +-- cycle-diagram -- +1: "" { + shape: cycle + a -> b -> c -> d +} +2: "" { + shape: cycle + a -> b -> c +} +3: "" { + shape: cycle + a -> b +}