From 0b1d33e3ef369a1eb106c041a9da628e923eeb3b Mon Sep 17 00:00:00 2001 From: Yusuke Kimoto Date: Wed, 5 Aug 2026 12:27:31 +0900 Subject: [PATCH] d2cycle: route 'shape: cycle' edges as circular arcs clipped at shape borders Fixes #1578 Generates cycle-layout edge routes analytically as cubic Bezier arc segments that start/stop exactly on shape borders (bounding box for rectangular shapes, Perimeter() for circle/hexagon/etc), so no renderer changes are needed. Snaps node positions to integers so export-time truncation cannot shift boxes off the float route endpoints. Guards single-node radius divergence and missing border crossings. Co-Authored-By: Claude Fable 5 --- ci/release/changelogs/next.md | 1 + d2graph/cyclediagram.go | 7 + d2layouts/d2cycle/layout.go | 264 ++++++ d2layouts/d2cycle/layout_test.go | 140 +++ d2layouts/d2layouts.go | 10 + d2target/d2target.go | 3 + .../txtar/cycle-diagram/dagre/board.exp.json | 884 ++++++++++++++++++ .../txtar/cycle-diagram/dagre/sketch.exp.svg | 95 ++ .../txtar/cycle-diagram/elk/board.exp.json | 884 ++++++++++++++++++ .../txtar/cycle-diagram/elk/sketch.exp.svg | 95 ++ e2etests/txtar.txt | 14 + 11 files changed, 2397 insertions(+) create mode 100644 d2graph/cyclediagram.go create mode 100644 d2layouts/d2cycle/layout.go create mode 100644 d2layouts/d2cycle/layout_test.go create mode 100644 e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json create mode 100644 e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json create mode 100644 e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 9f57e22b89..4afba7e6c8 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,6 +1,7 @@ #### Features 🚀 - exports: gif exports work with `animate: true` keyword [#2663](https://github.com/d2lang/d2/pull/2663) +- d2layouts: `shape: cycle` arranges objects in a circle and routes edges as circular arcs that start and end at shape borders [#1578](https://github.com/d2lang/d2/issues/1578) #### Improvements 🧹 diff --git a/d2graph/cyclediagram.go b/d2graph/cyclediagram.go new file mode 100644 index 0000000000..a8f8e0b1ad --- /dev/null +++ b/d2graph/cyclediagram.go @@ -0,0 +1,7 @@ +package d2graph + +import "oss.terrastruct.com/d2/d2target" + +func (obj *Object) IsCycleDiagram() bool { + return obj != nil && obj.Shape.Value == d2target.ShapeCycleDiagram +} diff --git a/d2layouts/d2cycle/layout.go b/d2layouts/d2cycle/layout.go new file mode 100644 index 0000000000..ca5d99ce23 --- /dev/null +++ b/d2layouts/d2cycle/layout.go @@ -0,0 +1,264 @@ +package d2cycle + +import ( + "context" + "math" + + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/label" + "oss.terrastruct.com/util-go/go2" +) + +const ( + MIN_RADIUS = 200 + PADDING = 20 + + // number of chords used to search for the arc/border crossing + BORDER_SEARCH_STEPS = 100 + // bisection iterations to refine the crossing angle + BORDER_REFINE_STEPS = 30 +) + +// Layout arranges the graph's root objects on a circle and routes each edge +// as a circular arc that starts and ends exactly on the shape borders. +func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error { + objects := g.Root.ChildrenArray + if len(objects) == 0 { + return nil + } + + for _, obj := range g.Objects { + positionLabelsIcons(obj) + } + + radius := calculateRadius(objects) + positionObjects(objects, radius) + + for _, edge := range g.Edges { + createCircularArc(edge) + } + + return nil +} + +func calculateRadius(objects []*d2graph.Object) float64 { + if len(objects) < 2 { + return MIN_RADIUS + } + numObjects := float64(len(objects)) + maxSize := 0.0 + for _, obj := range objects { + size := math.Max(obj.Box.Width, obj.Box.Height) + maxSize = math.Max(maxSize, size) + } + // ensure neighboring objects don't overlap + minRadius := (maxSize/2.0 + PADDING) / math.Sin(math.Pi/numObjects) + return math.Max(minRadius, MIN_RADIUS) +} + +func positionObjects(objects []*d2graph.Object, radius float64) { + numObjects := float64(len(objects)) + // offset so the first object is at the top-center + angleOffset := -math.Pi / 2 + + for i, obj := range objects { + angle := angleOffset + (2 * math.Pi * float64(i) / numObjects) + + x := radius * math.Cos(angle) + y := radius * math.Sin(angle) + + // center the box at (x, y), snapped to integer coordinates so the + // box is not shifted later when positions are truncated for export + obj.TopLeft = geo.NewPoint( + math.Round(x-obj.Box.Width/2), + math.Round(y-obj.Box.Height/2), + ) + } +} + +// createCircularArc routes an edge as a circular arc on the layout circle. +// The arc is clipped so it starts on the source shape's border and ends on +// the destination shape's border, then emitted as cubic Bézier segments so +// it renders perfectly smooth. +func createCircularArc(edge *d2graph.Edge) { + if edge.Src == nil || edge.Dst == nil || + edge.Src.TopLeft == nil || edge.Dst.TopLeft == nil { + return + } + + srcCenter := edge.Src.Center() + dstCenter := edge.Dst.Center() + + srcAngle := math.Atan2(srcCenter.Y, srcCenter.X) + dstAngle := math.Atan2(dstCenter.Y, dstCenter.X) + // always route the arc in the direction of increasing angle + // (this also makes a self-referencing edge a full loop) + if dstAngle <= srcAngle { + dstAngle += 2 * math.Pi + } + + arcRadius := (math.Hypot(srcCenter.X, srcCenter.Y) + math.Hypot(dstCenter.X, dstCenter.Y)) / 2 + + // clip the arc to the shape borders + startAngle, foundStart := findBorderCrossing(edge.Src, arcRadius, srcAngle, dstAngle) + endAngle, foundEnd := findBorderCrossing(edge.Dst, arcRadius, dstAngle, srcAngle) + if !foundStart || !foundEnd || startAngle >= endAngle { + // fallback: keep the center-to-center arc + startAngle = srcAngle + endAngle = dstAngle + } + + edge.Route = arcToBeziers(arcRadius, startAngle, endAngle) + edge.IsCurve = true + + if edge.Label.Value != "" && edge.LabelPosition == nil { + edge.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String()) + } +} + +// findBorderCrossing finds the angle at which the circle of arcRadius +// (centered at the origin) crosses obj's border, walking from fromAngle +// (at obj's center, inside obj) toward toAngle. It reports whether a +// crossing was found. +func findBorderCrossing(obj *d2graph.Object, arcRadius, fromAngle, toAngle float64) (float64, bool) { + shape := obj.ToShape() + var perimeter []geo.Intersectable + if shape.Is("") || shape.IsRectangular() { + // rectangular shapes are clipped at their bounding box + // (they don't define a Perimeter, see shape.TraceToShapeBorder) + box := shape.GetBox() + tl := box.TopLeft + tr := geo.NewPoint(tl.X+box.Width, tl.Y) + br := geo.NewPoint(tl.X+box.Width, tl.Y+box.Height) + bl := geo.NewPoint(tl.X, tl.Y+box.Height) + perimeter = []geo.Intersectable{ + *geo.NewSegment(tl, tr), + *geo.NewSegment(tr, br), + *geo.NewSegment(br, bl), + *geo.NewSegment(bl, tl), + } + } else { + perimeter = shape.Perimeter() + } + if len(perimeter) == 0 { + return 0, false + } + + arcPoint := func(angle float64) *geo.Point { + return geo.NewPoint(arcRadius*math.Cos(angle), arcRadius*math.Sin(angle)) + } + crosses := func(a, b float64) bool { + seg := *geo.NewSegment(arcPoint(a), arcPoint(b)) + for _, side := range perimeter { + if len(side.Intersections(seg)) > 0 { + return true + } + } + return false + } + + // walk chords from the center outward until one crosses the border + step := (toAngle - fromAngle) / BORDER_SEARCH_STEPS + var lo, hi float64 + found := false + for i := 0; i < BORDER_SEARCH_STEPS; i++ { + a := fromAngle + float64(i)*step + b := a + step + if crosses(a, b) { + lo, hi = a, b + found = true + break + } + } + if !found { + return 0, false + } + + // bisect [lo, hi] down to the exact crossing angle + // invariant: the crossing stays inside [lo, hi] + for i := 0; i < BORDER_REFINE_STEPS; i++ { + mid := (lo + hi) / 2 + if crosses(lo, mid) { + hi = mid + } else { + lo = mid + } + } + return (lo + hi) / 2, true +} + +// arcToBeziers converts the circular arc between startAngle and endAngle +// (on the circle of the given radius centered at the origin) into a route of +// cubic Bézier segments: [P0, C1, C2, P1, C1, C2, P2, ...]. +func arcToBeziers(radius, startAngle, endAngle float64) []*geo.Point { + span := endAngle - startAngle + // one Bézier segment per quarter turn keeps the fit accurate + numSegments := int(math.Ceil(span / (math.Pi / 2))) + if numSegments < 1 { + numSegments = 1 + } + segmentSpan := span / float64(numSegments) + // standard circular-arc Bézier approximation constant + k := 4.0 / 3.0 * math.Tan(segmentSpan/4) + + arcPoint := func(angle float64) *geo.Point { + return geo.NewPoint(radius*math.Cos(angle), radius*math.Sin(angle)) + } + // unit tangent in the direction of increasing angle + tangent := func(angle float64) *geo.Point { + return geo.NewPoint(-math.Sin(angle), math.Cos(angle)) + } + + route := make([]*geo.Point, 0, 1+3*numSegments) + route = append(route, arcPoint(startAngle)) + for i := 0; i < numSegments; i++ { + a0 := startAngle + float64(i)*segmentSpan + a1 := a0 + segmentSpan + p0, p1 := arcPoint(a0), arcPoint(a1) + t0, t1 := tangent(a0), tangent(a1) + route = append(route, + geo.NewPoint(p0.X+k*radius*t0.X, p0.Y+k*radius*t0.Y), + geo.NewPoint(p1.X-k*radius*t1.X, p1.Y-k*radius*t1.Y), + p1, + ) + } + return route +} + +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..073ad99851 --- /dev/null +++ b/d2layouts/d2cycle/layout_test.go @@ -0,0 +1,140 @@ +package d2cycle_test + +import ( + "context" + "math" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "oss.terrastruct.com/d2/d2compiler" + "oss.terrastruct.com/d2/d2layouts/d2cycle" + "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/log" +) + +// borderDistance returns the distance from p to the border of box (0 if p is +// exactly on the border). +func borderDistance(box *geo.Box, p *geo.Point) float64 { + x, y := box.TopLeft.X, box.TopLeft.Y + r, b := x+box.Width, y+box.Height + dx := math.Max(math.Max(x-p.X, p.X-r), 0) + dy := math.Max(math.Max(y-p.Y, p.Y-b), 0) + if dx > 0 || dy > 0 { + // outside: distance to the box + return math.Hypot(dx, dy) + } + // inside: distance to the closest side + return math.Min( + math.Min(p.X-x, r-p.X), + math.Min(p.Y-y, b-p.Y), + ) +} + +func TestCycleLayout(t *testing.T) { + input := ` +shape: cycle +a -> b -> c -> d -> a +` + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) + assert.Nil(t, err) + for _, obj := range g.Root.ChildrenArray { + obj.Box = geo.NewBox(nil, 100, 60) + } + + ctx := log.WithTB(context.Background(), t) + err = d2cycle.Layout(ctx, g, nil) + assert.Nil(t, err) + + // nodes are centered on a common circle around the origin + radii := make([]float64, len(g.Root.ChildrenArray)) + for i, obj := range g.Root.ChildrenArray { + center := obj.Center() + radii[i] = math.Hypot(center.X, center.Y) + } + for i := 1; i < len(radii); i++ { + // TopLeft is snapped to integers so allow 1px of tolerance + assert.InDelta(t, radii[0], radii[i], 1.0) + } + + for _, edge := range g.Edges { + assert.True(t, edge.IsCurve) + // cubic Bézier route: [P0, C1, C2, P1, ...] + assert.Equal(t, 1, len(edge.Route)%3) + + // the route starts and ends exactly on the shape borders + start := edge.Route[0] + end := edge.Route[len(edge.Route)-1] + assert.Less(t, borderDistance(edge.Src.Box, start), 0.01) + assert.Less(t, borderDistance(edge.Dst.Box, end), 0.01) + + // every anchor point lies on the same circle around the origin (the + // arc stays perfectly circular) + arcRadius := math.Hypot(start.X, start.Y) + for i := 3; i < len(edge.Route); i += 3 { + p := edge.Route[i] + assert.InDelta(t, arcRadius, math.Hypot(p.X, p.Y), 0.01) + } + } +} + +func TestCycleLayoutNonRectangular(t *testing.T) { + input := ` +shape: cycle +a: {shape: circle} +b: {shape: hexagon} +c +a -> b -> c -> a +` + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) + assert.Nil(t, err) + for _, obj := range g.Root.ChildrenArray { + obj.Box = geo.NewBox(nil, 80, 80) + } + + ctx := log.WithTB(context.Background(), t) + err = d2cycle.Layout(ctx, g, nil) + assert.Nil(t, err) + + a, has := g.Root.HasChild([]string{"a"}) + assert.True(t, has) + + for _, edge := range g.Edges { + assert.True(t, edge.IsCurve) + start := edge.Route[0] + end := edge.Route[len(edge.Route)-1] + + // arcs at a circle shape stop on the visible circle perimeter, not + // its bounding box + if edge.Src == a { + center := a.Center() + assert.InDelta(t, a.Width/2, math.Hypot(start.X-center.X, start.Y-center.Y), 0.5) + } + if edge.Dst == a { + center := a.Center() + assert.InDelta(t, a.Width/2, math.Hypot(end.X-center.X, end.Y-center.Y), 0.5) + } + } +} + +func TestCycleLayoutSingleNode(t *testing.T) { + // a single node must not produce an infinite radius + input := ` +shape: cycle +a +` + g, _, err := d2compiler.Compile("", strings.NewReader(input), nil) + assert.Nil(t, err) + for _, obj := range g.Root.ChildrenArray { + obj.Box = geo.NewBox(nil, 100, 60) + } + + ctx := log.WithTB(context.Background(), t) + err = d2cycle.Layout(ctx, g, nil) + assert.Nil(t, err) + + obj := g.Root.ChildrenArray[0] + assert.False(t, math.IsInf(obj.TopLeft.X, 0)) + assert.False(t, math.IsInf(obj.TopLeft.Y, 0)) +} diff --git a/d2layouts/d2layouts.go b/d2layouts/d2layouts.go index c0d41e3973..8ab6acf8e2 100644 --- a/d2layouts/d2layouts.go +++ b/d2layouts/d2layouts.go @@ -9,6 +9,7 @@ import ( "strings" "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2layouts/d2cycle" "oss.terrastruct.com/d2/d2layouts/d2grid" "oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/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 { @@ -260,6 +262,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) @@ -364,6 +372,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/d2target/d2target.go b/d2target/d2target.go index 63fcfacbf3..08f130c803 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -1072,6 +1072,7 @@ const ( ShapeSQLTable = "sql_table" ShapeImage = "image" ShapeSequenceDiagram = "sequence_diagram" + ShapeCycleDiagram = "cycle" ShapeHierarchy = "hierarchy" ) @@ -1100,6 +1101,7 @@ var Shapes = []string{ ShapeSQLTable, ShapeImage, ShapeSequenceDiagram, + ShapeCycleDiagram, ShapeHierarchy, } @@ -1170,6 +1172,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..3004ae4a13 --- /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": 454, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "1.a", + "type": "rectangle", + "pos": { + "x": -26, + "y": -233 + }, + "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": 174, + "y": -33 + }, + "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": -26, + "y": 167 + }, + "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": -227, + "y": -33 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2", + "type": "cycle", + "pos": { + "x": 514, + "y": 50 + }, + "width": 400, + "height": 366, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "2.a", + "type": "rectangle", + "pos": { + "x": 488, + "y": -183 + }, + "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": 661, + "y": 117 + }, + "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": 314, + "y": 117 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3", + "type": "cycle", + "pos": { + "x": 974, + "y": 0 + }, + "width": 53, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "3.a", + "type": "rectangle", + "pos": { + "x": 948, + "y": -233 + }, + "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": 948, + "y": 167 + }, + "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": 26.999000549316406, + "y": -198.42100524902344 + }, + { + "x": 113.94000244140625, + "y": -186.59100341796875 + }, + { + "x": 183.05299377441406, + "y": -119.54100036621094 + }, + { + "x": 197.51199340820312, + "y": -33 + } + ], + "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": 197.51199340820312, + "y": 33 + }, + { + "x": 183.05299377441406, + "y": 119.54100036621094 + }, + { + "x": 113.94000244140625, + "y": 186.59100341796875 + }, + { + "x": 26.999000549316406, + "y": 198.42100524902344 + } + ], + "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": -25.999000549316406, + "y": 198.30299377441406 + }, + { + "x": -113.22899627685547, + "y": 186.86599731445312 + }, + { + "x": -182.74200439453125, + "y": 119.7699966430664 + }, + { + "x": -197.25900268554688, + "y": 33 + } + ], + "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": 540.9990234375, + "y": -148.29800415039062 + }, + { + "x": 600.708984375, + "y": -140.16799926757812 + }, + { + "x": 653.5759887695312, + "y": -105.56600189208984 + }, + { + "x": 684.9219970703125, + "y": -54.0989990234375 + }, + { + "x": 716.2670288085938, + "y": -2.632999897003174 + }, + { + "x": 722.7529907226562, + "y": 60.21699905395508 + }, + { + "x": 702.5789794921875, + "y": 117 + } + ], + "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": 663.7100219726562, + "y": 183 + }, + { + "x": 625.7069702148438, + "y": 225.77699279785156 + }, + { + "x": 571.219970703125, + "y": 250.2550048828125 + }, + { + "x": 514, + "y": 250.2550048828125 + }, + { + "x": 456.77899169921875, + "y": 250.2550048828125 + }, + { + "x": 402.2919921875, + "y": 225.77699279785156 + }, + { + "x": 364.28900146484375, + "y": 183 + } + ], + "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": 1001, + "y": -198.16900634765625 + }, + { + "x": 1100.1070556640625, + "y": -184.66600036621094 + }, + { + "x": 1174, + "y": -100.02300262451172 + }, + { + "x": 1174, + "y": 0 + }, + { + "x": 1174, + "y": 100.02300262451172 + }, + { + "x": 1100.1070556640625, + "y": 184.66600036621094 + }, + { + "x": 1001, + "y": 198.16900634765625 + } + ], + "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..91f59882ac --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg @@ -0,0 +1,95 @@ +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..2c3b46d11e --- /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": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "1.a", + "type": "rectangle", + "pos": { + "x": -14, + "y": -221 + }, + "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": 186, + "y": -21 + }, + "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": -14, + "y": 179 + }, + "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": -215, + "y": -21 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "2", + "type": "cycle", + "pos": { + "x": 486, + "y": 62 + }, + "width": 400, + "height": 366, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "2.a", + "type": "rectangle", + "pos": { + "x": 460, + "y": -171 + }, + "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": 633, + "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": "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": 286, + "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": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "3", + "type": "cycle", + "pos": { + "x": 906, + "y": 12 + }, + "width": 53, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N7", + "stroke": "B1", + "animated": false, + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "zIndex": 0, + "level": 1 + }, + { + "id": "3.a", + "type": "rectangle", + "pos": { + "x": 880, + "y": -221 + }, + "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": 880, + "y": 179 + }, + "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": 38.999000549316406, + "y": -186.42100524902344 + }, + { + "x": 125.94000244140625, + "y": -174.59100341796875 + }, + { + "x": 195.05299377441406, + "y": -107.54100036621094 + }, + { + "x": 209.51199340820312, + "y": -21 + } + ], + "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": 209.51199340820312, + "y": 45 + }, + { + "x": 195.05299377441406, + "y": 131.54100036621094 + }, + { + "x": 125.94000244140625, + "y": 198.59100341796875 + }, + { + "x": 38.999000549316406, + "y": 210.42100524902344 + } + ], + "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": -13.99899959564209, + "y": 210.30299377441406 + }, + { + "x": -101.22899627685547, + "y": 198.86599731445312 + }, + { + "x": -170.74200439453125, + "y": 131.77000427246094 + }, + { + "x": -185.25900268554688, + "y": 45 + } + ], + "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": 512.9990234375, + "y": -136.29800415039062 + }, + { + "x": 572.708984375, + "y": -128.16799926757812 + }, + { + "x": 625.5759887695312, + "y": -93.56600189208984 + }, + { + "x": 656.9219970703125, + "y": -42.0989990234375 + }, + { + "x": 688.2670288085938, + "y": 9.366000175476074 + }, + { + "x": 694.7529907226562, + "y": 72.21700286865234 + }, + { + "x": 674.5789794921875, + "y": 129 + } + ], + "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": 635.7100219726562, + "y": 195 + }, + { + "x": 597.7069702148438, + "y": 237.77699279785156 + }, + { + "x": 543.219970703125, + "y": 262.2550048828125 + }, + { + "x": 486, + "y": 262.2550048828125 + }, + { + "x": 428.77899169921875, + "y": 262.2550048828125 + }, + { + "x": 374.2919921875, + "y": 237.77699279785156 + }, + { + "x": 336.28900146484375, + "y": 195 + } + ], + "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": 933, + "y": -186.16900634765625 + }, + { + "x": 1032.1070556640625, + "y": -172.66600036621094 + }, + { + "x": 1106, + "y": -88.02300262451172 + }, + { + "x": 1106, + "y": 12 + }, + { + "x": 1106, + "y": 112.02300262451172 + }, + { + "x": 1032.1070556640625, + "y": 196.66600036621094 + }, + { + "x": 933, + "y": 210.16900634765625 + } + ], + "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..c5150f620a --- /dev/null +++ b/e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg @@ -0,0 +1,95 @@ +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 +}