diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 2b477d515..b3e28446a 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -9,14 +9,16 @@ Hope everyone is enjoying the holidays this week! #### Features ๐Ÿš€ - `sketch` flag renders the diagram to look like it was sketched by hand. [#492](https://github.com/terrastruct/d2/pull/492) +- `near` now takes constants like `top-center`, particularly useful for diagram titles. See [docs](https://d2lang.com/tour/text#near-a-constant) for more. [#525](https://github.com/terrastruct/d2/pull/525) #### Improvements ๐Ÿงน - Improved label placements for shapes with images and icons to avoid overlapping labels. [#474](https://github.com/terrastruct/d2/pull/474) -- Themes are applied to sql_table and class shapes. [#521](https://github.com/terrastruct/d2/pull/521) +- Themes are applied to `sql_table` and `class` shapes. [#521](https://github.com/terrastruct/d2/pull/521) - `class` shapes use monospaced font. [#521](https://github.com/terrastruct/d2/pull/521) - Sequence diagram edge group labels have more reasonable padding. [#512](https://github.com/terrastruct/d2/pull/512) - ELK layout engine preserves order of nodes. [#282](https://github.com/terrastruct/d2/issues/282) +- Markdown headings set font-family explicitly, so that external stylesheets with more specific targeting don't override it. [#525](https://github.com/terrastruct/d2/pull/525) #### Bugfixes โ›‘๏ธ diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 9ca656098..9ca373d81 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -850,9 +850,14 @@ func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ast.Map) { func (c *compiler) validateNear(g *d2graph.Graph) { for _, obj := range g.Objects { if obj.Attributes.NearKey != nil { - _, ok := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) - if !ok { - c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "near key %#v does not exist. It must be the absolute path to a shape.", d2format.Format(obj.Attributes.NearKey)) + _, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) + _, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]] + if !isKey && !isConst { + c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.Attributes.NearKey), strings.Join(d2graph.NearConstantsArray, ", ")) + continue + } + if !isKey && isConst && obj.Parent != g.Root { + c.errorf(obj.Attributes.NearKey.GetRange().Start, obj.Attributes.NearKey.GetRange().End, "constant near keys can only be set on root level shapes") continue } } diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index afe9ceec4..d6459f34d 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -1266,6 +1266,28 @@ x -> y: { } }, }, + { + name: "near_constant", + + text: `x.near: top-center +`, + }, + { + name: "near_bad_constant", + + text: `x.near: txop-center +`, + expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:1: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right +`, + }, + { + name: "nested_near_constant", + + text: `x.y.near: top-center +`, + expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:1: constant near keys can only be set on root level shapes +`, + }, { name: "reserved_icon_near_style", @@ -1312,7 +1334,7 @@ y expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:4:18: expected "opacity" to be a number between 0.0 and 1.0 d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0 -d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" does not exist. It must be the absolute path to a shape. +d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right `, }, { diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index e60aa17c0..ebaf0274c 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -1189,6 +1189,22 @@ var StyleKeywords = map[string]struct{}{ "filled": {}, } +// TODO maybe autofmt should allow other values, and transform them to conform +// e.g. left-center becomes center-left +var NearConstantsArray = []string{ + "top-left", + "top-center", + "top-right", + + "center-left", + "center-right", + + "bottom-left", + "bottom-center", + "bottom-right", +} +var NearConstants map[string]struct{} + func init() { for k, v := range StyleKeywords { ReservedKeywords[k] = v @@ -1196,4 +1212,8 @@ func init() { for k, v := range ReservedKeywordHolders { ReservedKeywords[k] = v } + NearConstants = make(map[string]struct{}, len(NearConstantsArray)) + for _, k := range NearConstantsArray { + NearConstants[k] = struct{}{} + } } diff --git a/d2layouts/d2near/layout.go b/d2layouts/d2near/layout.go new file mode 100644 index 000000000..99e8a10e1 --- /dev/null +++ b/d2layouts/d2near/layout.go @@ -0,0 +1,146 @@ +// d2near applies near keywords when they're constants +// Intended to be run as the last stage of layout after the diagram has already undergone layout +package d2near + +import ( + "context" + "math" + "strings" + + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/label" + "oss.terrastruct.com/util-go/go2" +) + +const pad = 20 + +// Layout finds the shapes which are assigned constant near keywords and places them. +func Layout(ctx context.Context, g *d2graph.Graph, constantNears []*d2graph.Object) error { + if len(constantNears) == 0 { + return nil + } + + // Imagine the graph has two long texts, one at top center and one at top left. + // Top left should go left enough to not collide with center. + // So place the center ones first, then the later ones will consider them for bounding box + for _, processCenters := range []bool{true, false} { + for _, obj := range constantNears { + if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "center") { + obj.TopLeft = geo.NewPoint(place(obj)) + } + } + for _, obj := range constantNears { + if processCenters == strings.Contains(d2graph.Key(obj.Attributes.NearKey)[0], "center") { + // The z-index for constant nears does not matter, as it will not collide + g.Objects = append(g.Objects, obj) + obj.Parent.Children[obj.ID] = obj + obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray, obj) + } + } + } + + // These shapes skipped core layout, which means they also skipped label placements + for _, obj := range constantNears { + if obj.Attributes.Shape.Value == d2target.ShapeImage { + obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) + } else if obj.Attributes.Icon != nil { + obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) + } else { + obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) + } + } + + return nil +} + +// place returns the position of obj, taking into consideration its near value and the diagram +func place(obj *d2graph.Object) (float64, float64) { + tl, br := boundingBox(obj.Graph) + w := br.X - tl.X + h := br.Y - tl.Y + switch d2graph.Key(obj.Attributes.NearKey)[0] { + case "top-left": + return tl.X - obj.Width - pad, tl.Y - obj.Height - pad + case "top-center": + return tl.X + w/2 - obj.Width/2, tl.Y - obj.Height - pad + case "top-right": + return br.X + pad, tl.Y - obj.Height - pad + case "center-left": + return tl.X - obj.Width - pad, tl.Y + h/2 - obj.Height/2 + case "center-right": + return br.X + pad, tl.Y + h/2 - obj.Height/2 + case "bottom-left": + return tl.X - obj.Width - pad, br.Y + pad + case "bottom-center": + return br.X - w/2 - obj.Width/2, br.Y + pad + case "bottom-right": + return br.X + pad, br.Y + pad + } + return 0, 0 +} + +// WithoutConstantNears plucks out the graph objects which have "near" set to a constant value +// This is to be called before layout engines so they don't take part in regular positioning +func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (nears []*d2graph.Object) { + for i := 0; i < len(g.Objects); i++ { + obj := g.Objects[i] + if obj.Attributes.NearKey == nil { + continue + } + _, isKey := g.Root.HasChild(d2graph.Key(obj.Attributes.NearKey)) + if isKey { + continue + } + _, isConst := d2graph.NearConstants[d2graph.Key(obj.Attributes.NearKey)[0]] + if isConst { + nears = append(nears, obj) + g.Objects = append(g.Objects[:i], g.Objects[i+1:]...) + i-- + delete(obj.Parent.Children, obj.ID) + for i := 0; i < len(obj.Parent.ChildrenArray); i++ { + if obj.Parent.ChildrenArray[i] == obj { + obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray[:i], obj.Parent.ChildrenArray[i+1:]...) + break + } + } + } + } + return nears +} + +// boundingBox gets the center of the graph as defined by shapes +// The bounds taking into consideration only shapes gives more of a feeling of true center +// It differs from d2target.BoundingBox which needs to include every visible thing +func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) { + if len(g.Objects) == 0 { + return geo.NewPoint(0, 0), geo.NewPoint(0, 0) + } + x1 := math.Inf(1) + y1 := math.Inf(1) + x2 := math.Inf(-1) + y2 := math.Inf(-1) + + for _, obj := range g.Objects { + if obj.Attributes.NearKey != nil { + // Top left should not be MORE top than top-center + // But it should go more left if top-center label extends beyond bounds of diagram + switch d2graph.Key(obj.Attributes.NearKey)[0] { + case "top-center", "bottom-center": + x1 = math.Min(x1, obj.TopLeft.X) + x2 = math.Max(x2, obj.TopLeft.X+obj.Width) + case "center-left", "center-right": + y1 = math.Min(y1, obj.TopLeft.Y) + y2 = math.Max(y2, obj.TopLeft.Y+obj.Height) + } + } else { + x1 = math.Min(x1, obj.TopLeft.X) + y1 = math.Min(y1, obj.TopLeft.Y) + x2 = math.Max(x2, obj.TopLeft.X+obj.Width) + y2 = math.Max(y2, obj.TopLeft.Y+obj.Height) + } + } + + return geo.NewPoint(x1, y1), geo.NewPoint(x2, y2) +} diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 55192ceab..1747c859b 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -69,6 +69,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove) g.Edges = layoutEdges layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove) + // TODO this isn't a proper deletion because the objects still appear as children of the object g.Objects = layoutObjects if g.Root.IsSequenceDiagram() { diff --git a/d2lib/d2.go b/d2lib/d2.go index be71a1536..936124b9f 100644 --- a/d2lib/d2.go +++ b/d2lib/d2.go @@ -9,6 +9,7 @@ import ( "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2target" @@ -48,9 +49,20 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target return nil, nil, err } - if layout, err := getLayout(opts); err != nil { + coreLayout, err := getLayout(opts) + if err != nil { return nil, nil, err - } else if err := d2sequence.Layout(ctx, g, layout); err != nil { + } + + constantNears := d2near.WithoutConstantNears(ctx, g) + + err = d2sequence.Layout(ctx, g, coreLayout) + if err != nil { + return nil, nil, err + } + + err = d2near.Layout(ctx, g, constantNears) + if err != nil { return nil, nil, err } } diff --git a/d2renderers/d2sketch/testdata/twitter/sketch.exp.svg b/d2renderers/d2sketch/testdata/twitter/sketch.exp.svg index 471e8fa0a..f93b93c1f 100644 --- a/d2renderers/d2sketch/testdata/twitter/sketch.exp.svg +++ b/d2renderers/d2sketch/testdata/twitter/sketch.exp.svg @@ -282,6 +282,7 @@ width="3454" height="2449" viewBox="-100 -100 3454 2449">xy

The top of the mountain

+
JoeDonald

Cats, no less liquid than their shadows, offer no angles to the wind.

+

If we can't fix it, it ain't broke.

+

Dieters live life in the fasting lane.

+

i am top left

+

i am top right

+

i am bottom left

+

i am bottom right

+
+ + + \ No newline at end of file diff --git a/e2etests/testdata/stable/constant_near_stress/elk/board.exp.json b/e2etests/testdata/stable/constant_near_stress/elk/board.exp.json new file mode 100644 index 000000000..3bde4523c --- /dev/null +++ b/e2etests/testdata/stable/constant_near_stress/elk/board.exp.json @@ -0,0 +1,447 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "x", + "type": "", + "pos": { + "x": 12, + "y": 12 + }, + "width": 113, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "y", + "type": "", + "pos": { + "x": 12, + "y": 238 + }, + "width": 114, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "y", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 14, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "The top of the mountain", + "type": "text", + "pos": { + "x": -12, + "y": -32 + }, + "width": 162, + "height": 24, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "The top of the mountain", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 162, + "labelHeight": 24, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "Joe", + "type": "person", + "pos": { + "x": -139, + "y": 125 + }, + "width": 131, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Joe", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 31, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "Donald", + "type": "person", + "pos": { + "x": 146, + "y": 125 + }, + "width": 155, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Donald", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 55, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "bottom", + "type": "text", + "pos": { + "x": -402, + "y": 384 + }, + "width": 943, + "height": 130, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "# Cats, no less liquid than their shadows, offer no angles to the wind.\n\nIf we can't fix it, it ain't broke.\n\nDieters live life in the fasting lane.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 943, + "labelHeight": 130, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "i am top left", + "type": "text", + "pos": { + "x": -503, + "y": -32 + }, + "width": 81, + "height": 24, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "i am top left", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 81, + "labelHeight": 24, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "i am top right", + "type": "text", + "pos": { + "x": 560, + "y": -32 + }, + "width": 91, + "height": 24, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "i am top right", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 91, + "labelHeight": 24, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "i am bottom left", + "type": "text", + "pos": { + "x": -530, + "y": 384 + }, + "width": 108, + "height": 24, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "i am bottom left", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 108, + "labelHeight": 24, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "i am bottom right", + "type": "text", + "pos": { + "x": 560, + "y": 384 + }, + "width": 118, + "height": 24, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "i am bottom right", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 118, + "labelHeight": 24, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(x -> y)[0]", + "src": "x", + "srcArrow": "none", + "srcLabel": "", + "dst": "y", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 69, + "y": 138 + }, + { + "x": 69, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/constant_near_stress/elk/sketch.exp.svg b/e2etests/testdata/stable/constant_near_stress/elk/sketch.exp.svg new file mode 100644 index 000000000..646e4a067 --- /dev/null +++ b/e2etests/testdata/stable/constant_near_stress/elk/sketch.exp.svg @@ -0,0 +1,803 @@ + +xy

The top of the mountain

+
JoeDonald

Cats, no less liquid than their shadows, offer no angles to the wind.

+

If we can't fix it, it ain't broke.

+

Dieters live life in the fasting lane.

+

i am top left

+

i am top right

+

i am bottom left

+

i am bottom right

+
+ + +
\ No newline at end of file diff --git a/e2etests/testdata/stable/constant_near_title/dagre/board.exp.json b/e2etests/testdata/stable/constant_near_title/dagre/board.exp.json new file mode 100644 index 000000000..68001d02d --- /dev/null +++ b/e2etests/testdata/stable/constant_near_title/dagre/board.exp.json @@ -0,0 +1,500 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "poll the people", + "type": "", + "pos": { + "x": 112, + "y": 0 + }, + "width": 210, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "poll the people", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 110, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "results", + "type": "", + "pos": { + "x": 241, + "y": 226 + }, + "width": 153, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "results", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 53, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "unfavorable", + "type": "", + "pos": { + "x": 0, + "y": 452 + }, + "width": 191, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "unfavorable", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 91, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "favorable", + "type": "", + "pos": { + "x": 251, + "y": 452 + }, + "width": 173, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "favorable", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 73, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "will of the people", + "type": "", + "pos": { + "x": 224, + "y": 678 + }, + "width": 227, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "will of the people", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 127, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "title", + "type": "text", + "pos": { + "x": 92, + "y": -70 + }, + "width": 266, + "height": 50, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "# A winning strategy", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 266, + "labelHeight": 50, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(poll the people -> results)[0]", + "src": "poll the people", + "srcArrow": "none", + "srcLabel": "", + "dst": "results", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 272.80973451327435, + "y": 126 + }, + { + "x": 308.5619469026549, + "y": 166 + }, + { + "x": 317.5, + "y": 186 + }, + { + "x": 317.5, + "y": 226 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(results -> unfavorable)[0]", + "src": "results", + "srcArrow": "none", + "srcLabel": "", + "dst": "unfavorable", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 250.03982300884957, + "y": 352 + }, + { + "x": 207.2079646017699, + "y": 392 + }, + { + "x": 187.5, + "y": 412 + }, + { + "x": 151.5, + "y": 452 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(unfavorable -> poll the people)[0]", + "src": "unfavorable", + "srcArrow": "none", + "srcLabel": "", + "dst": "poll the people", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 84.34955752212389, + "y": 452 + }, + { + "x": 77.26991150442478, + "y": 412 + }, + { + "x": 75.5, + "y": 379.4 + }, + { + "x": 75.5, + "y": 345.5 + }, + { + "x": 75.5, + "y": 311.6 + }, + { + "x": 87.9, + "y": 166 + }, + { + "x": 137.5, + "y": 126 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(results -> favorable)[0]", + "src": "results", + "srcArrow": "none", + "srcLabel": "", + "dst": "favorable", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 328.6504424778761, + "y": 352 + }, + { + "x": 335.7300884955752, + "y": 392 + }, + { + "x": 337.5, + "y": 412 + }, + { + "x": 337.5, + "y": 452 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(favorable -> will of the people)[0]", + "src": "favorable", + "srcArrow": "none", + "srcLabel": "", + "dst": "will of the people", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 337.5, + "y": 578 + }, + { + "x": 337.5, + "y": 618 + }, + { + "x": 337.5, + "y": 638 + }, + { + "x": 337.5, + "y": 678 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/constant_near_title/dagre/sketch.exp.svg b/e2etests/testdata/stable/constant_near_title/dagre/sketch.exp.svg new file mode 100644 index 000000000..bfd53ca71 --- /dev/null +++ b/e2etests/testdata/stable/constant_near_title/dagre/sketch.exp.svg @@ -0,0 +1,796 @@ + +poll the peopleresultsunfavorablefavorablewill of the people

A winning strategy

+
+ + +
\ No newline at end of file diff --git a/e2etests/testdata/stable/constant_near_title/elk/board.exp.json b/e2etests/testdata/stable/constant_near_title/elk/board.exp.json new file mode 100644 index 000000000..cfd0c8a4f --- /dev/null +++ b/e2etests/testdata/stable/constant_near_title/elk/board.exp.json @@ -0,0 +1,459 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "poll the people", + "type": "", + "pos": { + "x": 81, + "y": 12 + }, + "width": 210, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "poll the people", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 110, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "results", + "type": "", + "pos": { + "x": 74, + "y": 238 + }, + "width": 153, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "results", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 53, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "unfavorable", + "type": "", + "pos": { + "x": 232, + "y": 464 + }, + "width": 191, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "unfavorable", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 91, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "favorable", + "type": "", + "pos": { + "x": 39, + "y": 464 + }, + "width": 173, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "favorable", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 73, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "will of the people", + "type": "", + "pos": { + "x": 12, + "y": 690 + }, + "width": 227, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "will of the people", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 127, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "title", + "type": "text", + "pos": { + "x": 84, + "y": -58 + }, + "width": 266, + "height": 50, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "transparent", + "stroke": "#0A0F25", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "# A winning strategy", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "markdown", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 266, + "labelHeight": 50, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(poll the people -> results)[0]", + "src": "poll the people", + "srcArrow": "none", + "srcLabel": "", + "dst": "results", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 151, + "y": 138 + }, + { + "x": 151, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(results -> unfavorable)[0]", + "src": "results", + "srcArrow": "none", + "srcLabel": "", + "dst": "unfavorable", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 176.5, + "y": 364 + }, + { + "x": 176.5, + "y": 414 + }, + { + "x": 295.6666666666667, + "y": 414 + }, + { + "x": 295.6666666666667, + "y": 464 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(unfavorable -> poll the people)[0]", + "src": "unfavorable", + "srcArrow": "none", + "srcLabel": "", + "dst": "poll the people", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 359.3333333333333, + "y": 464 + }, + { + "x": 359.3333333333333, + "y": 188 + }, + { + "x": 221, + "y": 188 + }, + { + "x": 221, + "y": 138 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(results -> favorable)[0]", + "src": "results", + "srcArrow": "none", + "srcLabel": "", + "dst": "favorable", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 125.5, + "y": 364 + }, + { + "x": 125.5, + "y": 464 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(favorable -> will of the people)[0]", + "src": "favorable", + "srcArrow": "none", + "srcLabel": "", + "dst": "will of the people", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 125.5, + "y": 590 + }, + { + "x": 125.5, + "y": 690 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/constant_near_title/elk/sketch.exp.svg b/e2etests/testdata/stable/constant_near_title/elk/sketch.exp.svg new file mode 100644 index 000000000..e5ea8add8 --- /dev/null +++ b/e2etests/testdata/stable/constant_near_title/elk/sketch.exp.svg @@ -0,0 +1,796 @@ + +poll the peopleresultsunfavorablefavorablewill of the people

A winning strategy

+
+ + +
\ No newline at end of file diff --git a/e2etests/testdata/stable/giant_markdown_test/dagre/sketch.exp.svg b/e2etests/testdata/stable/giant_markdown_test/dagre/sketch.exp.svg index 53f05277f..3650e806e 100644 --- a/e2etests/testdata/stable/giant_markdown_test/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/giant_markdown_test/dagre/sketch.exp.svg @@ -277,6 +277,7 @@ width="3251" height="5500" viewBox="-100 -100 3251 5500">