diff --git a/README.md b/README.md
index ebf199e30..41b472a74 100644
--- a/README.md
+++ b/README.md
@@ -46,34 +46,63 @@ https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8
## What does D2 look like?
```d2
-# Actors
-hans: Hans Niemann
+vars: {
+ d2-config: {
+ layout-engine: elk
+ # Terminal theme code
+ theme-id: 300
+ }
+}
+network: {
+ cell tower: {
+ satellites: {
+ shape: stored_data
+ style.multiple: true
+ }
-defendants: {
- mc: Magnus Carlsen
- playmagnus: Play Magnus Group
- chesscom: Chess.com
- naka: Hikaru Nakamura
+ transmitter
- mc -> playmagnus: Owns majority
- playmagnus <-> chesscom: Merger talks
- chesscom -> naka: Sponsoring
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ satellites -> transmitter: send
+ }
+
+ online portal: {
+ ui: {shape: hexagon}
+ }
+
+ data processor: {
+ storage: {
+ shape: cylinder
+ style.multiple: true
+ }
+ }
+
+ cell tower.transmitter -> data processor.storage: phone logs
}
-# Accusations
-hans -> defendants: 'sueing for $100M'
+user: {
+ shape: person
+ width: 130
+}
-# Claim
-defendants.naka -> hans: Accused of cheating on his stream
-defendants.mc -> hans: Lost then withdrew with accusations
-defendants.chesscom -> hans: 72 page report of cheating
+user -> network.cell tower: make call
+user -> network.online portal.ui: access {
+ style.stroke-dash: 3
+}
+
+api server -> network.online portal.ui: display
+api server -> logs: persist
+logs: {shape: page; style.multiple: true}
+
+network.data processor -> api server
```
-> There is syntax highlighting with the editor plugins linked below.
+
+
+
-
-
-> Rendered with the TALA layout engine.
+> Open in [playground](https://play.d2lang.com/?script=rVLLTsQwDLznKyJxbrWwtyLxFdyR1Zg2ahpHibvLCvXfcdqGfSHthVv8yIxn7APE1OhvpbV5qVryn7ZbQ60dnGjiCn1nPTYa3bCkn_Q7xtF6cJp7HFG3ZHCpLGFlTaP3u51kZjUrj3ykOKyYLTr5REeMhSMBS84yppKRXA9B-BJTRPNhgKEU-OSwHifHNjjp4DitxLNa-SP4NFpmjOoGXVdvl2VBR2_-sWeZgLwTp3SgyOCKnsnKa5PU4xd05OfyIWvTIVKLKdHZExEOHd4Z0p4E3oi2h25s8Ge764uZs4Rr4vqXMfQkAhx1SVanplQWtU0QMCbyEh-t4b7Rz_td6cuo267ryzWPMMiFgHN3XVdu1dkmaPM8K-EiLnGkASsDScj2mQqCFcvj4RGUsSnI_d70Z2GrCptYrVHZTRADXv80VWgL0bVvGfJMoH4A)
> For more examples, see [./docs/examples](./docs/examples).
@@ -231,6 +260,7 @@ let us know and we'll be happy to include it here!
- **Logseq-D2**: [https://github.com/b-yp/logseq-d2](https://github.com/b-yp/logseq-d2)
- **ent2d2**: [https://github.com/tmc/ent2d2](https://github.com/tmc/ent2d2)
- **MkDocs Plugin**: [https://github.com/landmaj/mkdocs-d2-plugin](https://github.com/landmaj/mkdocs-d2-plugin)
+- **Remark Plugin**: [https://github.com/mech-a/remark-d2](https://github.com/mech-a/remark-d2)
### Misc
@@ -261,6 +291,12 @@ Do you have or see an open-source project with `.d2` files? Please submit a PR a
this selected list of featured projects using D2.
- [ElasticSearch](https://github.com/elastic/beats/blob/main/libbeat/publisher/queue/proxy/diagrams/broker.d2)
+- [Sourcegraph](https://handbook.sourcegraph.com/departments/engineering/managed-services/telemetry-gateway/#dev-architecture-diagram)
+- [Temporal](https://github.com/temporalio/temporal/blob/0be2681c994470c7c61ea88e4fcef89bb4024e58/docs/_assets/matching-context.d2)
+- [Tauri](https://v2.tauri.app/concept/inter-process-communication/)
+ - Rust GUI framework (78.5k stars)
+- [Intellij](https://github.com/JetBrains/intellij-community/blob/45bcfc17a3f3e0d8548bc69e922d4ca97ac21b2b/platform/settings/docs/topics/overview.md)
+- [Coder](https://coder.com/blog/managing-templates-in-coder)
- [UC
Berkeley](https://github.com/ucb-bar/hammer/blob/2b5c04d7b7d9ee3c73575efcd7ee0698bd5bfa88/doc/Hammer-Use/hier.d2)
- [Coronacheck](https://github.com/minvws/nl-covid19-coronacheck-app-ios/blob/e1567e9d1633b3273c537a105bff0e7d3a57ecfe/Diagrams/client-side-datamodel.d2)
diff --git a/ci/release/aws/ensure.sh b/ci/release/aws/ensure.sh
index 19ab6141b..794675998 100755
--- a/ci/release/aws/ensure.sh
+++ b/ci/release/aws/ensure.sh
@@ -241,8 +241,9 @@ create_windows_amd64() {
'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \
| jq -r '.Reservations[].Instances[].State.Name')
if [ -z "$state" ]; then
+ # public AMIs are deprecated every few months so just search the latest Windows Server one for recreating
sh_c aws ec2 run-instances \
- --image-id=ami-0c5300e833c2b32f3 \
+ --image-id=ami-03ea14ccbeab7b2d5 \
--count=1 \
--instance-type=t3.medium \
--security-groups=windows \
@@ -441,19 +442,22 @@ init_remote_windows() {
header "$REMOTE_NAME"
wait_remote_host_windows
+ # rsync was broken in this script last ran on 4/10/24.
+ # had to upgrade with `pacman -Syyu rsync` after
+
init_ps1=$(cat < script` and then open it with vim
FGCOLOR=3 bigheader "WARNING: WINDOWS INITIALIZATION MUST BE COMPLETED MANUALLY OVER RDP AND POWERSHELL!"
warn '1. Obtain Windows RDP password with:'
diff --git a/ci/release/build_in_docker.sh b/ci/release/build_in_docker.sh
index 034d842e4..cbfcbcdec 100755
--- a/ci/release/build_in_docker.sh
+++ b/ci/release/build_in_docker.sh
@@ -4,7 +4,7 @@ cd -- "$(dirname "$0")/../.."
. ./ci/sub/lib.sh
tag="$(sh_c docker build \
- --build-arg GOVERSION="1.20.8.linux-$ARCH" \
+ --build-arg GOVERSION="1.22.2.linux-$ARCH" \
-qf ./ci/release/linux/Dockerfile ./ci/release/linux)"
docker_run \
-e DRY_RUN \
diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 2d39449f0..56fb4e6b1 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -2,6 +2,14 @@
#### Improvements ๐งน
+- Opacity 0 shapes no longer have a label mask which made any segment of connections going through them lower opacity [#1940](https://github.com/terrastruct/d2/pull/1940)
+- Bidirectional connections are now animated in opposite directions rather than one direction [#1939](https://github.com/terrastruct/d2/pull/1939)
+
#### Bugfixes โ๏ธ
-- Fixes edge case of bad import syntax crashing using d2 as a library [1829](https://github.com/terrastruct/d2/pull/1829)
+- Local relative icons are relative to the d2 file instead of CLI invoke path [#1924](https://github.com/terrastruct/d2/pull/1924)
+- Custom label positions weren't being read when the width was smaller than the label [#1928](https://github.com/terrastruct/d2/pull/1928)
+- Using `shape: circle` for arrowheads no longer removes all arrowheads along path in sketch mode [#1942](https://github.com/terrastruct/d2/pull/1942)
+- Globs to null connections work [#1965](https://github.com/terrastruct/d2/pull/1965)
+- Edge globs setting styles inherit correctly in child boards [#1967](https://github.com/terrastruct/d2/pull/1967)
+- Board links imported with spread imports work [#1972](https://github.com/terrastruct/d2/pull/1972)
diff --git a/ci/release/changelogs/v0.6.4.md b/ci/release/changelogs/v0.6.4.md
new file mode 100644
index 000000000..69ef0268b
--- /dev/null
+++ b/ci/release/changelogs/v0.6.4.md
@@ -0,0 +1,19 @@
+#### Features ๐
+
+- `style.underline` works on connections [#1836](https://github.com/terrastruct/d2/pull/1836)
+- `none` is added as an accepted value for `fill-pattern`. Previously there was no way to cancel the `fill-pattern` on select objects set by a theme that applies it (Origami) [#1882](https://github.com/terrastruct/d2/pull/1882)
+
+#### Improvements ๐งน
+
+- Dimensions can be set less than label dimensions [#1901](https://github.com/terrastruct/d2/pull/1901)
+- Boards no longer inherit `label` fields from parents [#1838](https://github.com/terrastruct/d2/pull/1838)
+- Prevents `near` targeting a child of a special object like grid cells, which wasn't doing anything [#1851](https://github.com/terrastruct/d2/pull/1851)
+
+#### Bugfixes โ๏ธ
+
+- Theme flags on CLI apply to PDFs [#1894](https://github.com/terrastruct/d2/pull/1894)
+- Fixes styles in connections not overriding styles set by globs [#1857](https://github.com/terrastruct/d2/pull/1857)
+- Fixes `null` being set on a nested shape not working in certain cases when connections also pointed to that shape [#1830](https://github.com/terrastruct/d2/pull/1830)
+- Fixes edge case of bad import syntax crashing using d2 as a library [#1829](https://github.com/terrastruct/d2/pull/1829)
+- Fixes `style.fill` not applying to markdown [#1872](https://github.com/terrastruct/d2/pull/1872)
+- Fixes compiler erroring on certain styles when the shape's `shape` value is not all lowercase (e.g. `Circle`) [#1887](https://github.com/terrastruct/d2/pull/1887)
diff --git a/ci/release/changelogs/v0.6.5.md b/ci/release/changelogs/v0.6.5.md
new file mode 100644
index 000000000..2e5452a3e
--- /dev/null
+++ b/ci/release/changelogs/v0.6.5.md
@@ -0,0 +1,7 @@
+D2 0.6.5 has a hotfix for 0.6.4 breaking plugin compatibility. Also includes 2 compiler fixes regarding substitutions/vars.
+
+#### Bugfixes โ๏ธ
+
+- Fix executable plugins that implement standalone router [#1910](https://github.com/terrastruct/d2/pull/1910)
+- Fix compiler error with multiple nested spread substitutions [#1913](https://github.com/terrastruct/d2/pull/1913)
+- Fix substitutions from imports into different scopes [#1914](https://github.com/terrastruct/d2/pull/1914)
diff --git a/d2ast/d2ast.go b/d2ast/d2ast.go
index 85a530c88..705e840ed 100644
--- a/d2ast/d2ast.go
+++ b/d2ast/d2ast.go
@@ -862,9 +862,6 @@ func (mk *Key) HasTripleGlob() bool {
return true
}
}
- if mk.EdgeIndex != nil && mk.EdgeIndex.Glob {
- return true
- }
if mk.EdgeKey.HasTripleGlob() {
return true
}
@@ -1025,6 +1022,7 @@ type EdgeIndex struct {
}
func (ei1 *EdgeIndex) Equals(ei2 *EdgeIndex) bool {
+ // TODO probably should be checking the values, but will wait until something breaks to change
if ei1.Int != ei2.Int {
return false
}
diff --git a/d2chaos/d2chaos.go b/d2chaos/d2chaos.go
index 96f9cd906..337ac24ae 100644
--- a/d2chaos/d2chaos.go
+++ b/d2chaos/d2chaos.go
@@ -146,6 +146,10 @@ func (gs *dslGenState) edge() error {
}
}
+ if src == dst && gs.nodeShapes[dst] == d2target.ShapeSequenceDiagram {
+ return nil
+ }
+
srcArrow := "-"
if gs.randBool() {
srcArrow = "<"
@@ -265,7 +269,7 @@ func (gs *dslGenState) randStr(n int, inKey bool) string {
func (gs *dslGenState) randShape() string {
for {
s := shapes[gs.rand.Intn(len(shapes))]
- if s != d2target.ShapeImage {
+ if s != d2target.ShapeImage && s != d2target.ShapeText {
return s
}
}
diff --git a/d2cli/main.go b/d2cli/main.go
index 2b2c138d9..69c86bcd7 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -459,7 +459,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
if os.Getenv("D2_LSP_MODE") == "1" {
// only the parse result is needed if running d2 for lsp,
// if this, "fails", the AST is still valid and can be sent
- // to vscode extention
+ // to vscode extension
ast, err := d2lib.Parse(ctx, string(input), opts)
type LspOutputData struct {
@@ -529,7 +529,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
ext := getExportExtension(outputPath)
switch ext {
case GIF:
- svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
+ svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
if err != nil {
return nil, false, err
}
@@ -553,7 +553,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
path := []pdf.BoardTitle{
{Name: diagram.Root.Label, BoardID: "root"},
}
- pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
+ pdf, err := renderPDF(ctx, ms, plugin, renderOpts, inputPath, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
if err != nil {
return pdf, false, err
}
@@ -574,7 +574,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
path := []pptx.BoardTitle{
{Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
}
- svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
+ svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, inputPath, outputPath, page, diagram, path, boardIdToIndex)
if err != nil {
return nil, false, err
}
@@ -808,7 +808,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
if !diagram.IsFolderOnly {
start := time.Now()
- out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
+ out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return boards, err
}
@@ -824,7 +824,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
start := time.Now()
- out, err := _render(ctx, ms, plugin, opts, outputPath, bundle, forceAppendix, page, ruler, diagram)
+ out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return [][]byte{}, err
}
@@ -835,7 +835,7 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
return [][]byte{out}, nil
}
-func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
+func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := getExportExtension(outputPath) == PNG
var scale *float64
if opts.Scale != nil {
@@ -865,7 +865,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
l := simplelog.FromCmdLog(ms.Log)
- svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
+ svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
if bundle {
var bundleErr2 error
svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
@@ -915,7 +915,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
return svg, nil
}
-func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
+func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
var isRoot bool
if doc == nil {
doc = pdf.Init()
@@ -936,10 +936,11 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
}
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
- Pad: opts.Pad,
- Sketch: opts.Sketch,
- Center: opts.Center,
- Scale: scale,
+ Pad: opts.Pad,
+ Sketch: opts.Sketch,
+ Center: opts.Center,
+ Scale: scale,
+ ThemeID: opts.ThemeID,
})
if err != nil {
return nil, err
@@ -952,7 +953,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
l := simplelog.FromCmdLog(ms.Log)
- svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
+ svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil {
@@ -985,7 +986,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
Name: dl.Root.Label,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
})
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
+ _, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
if err != nil {
return nil, err
}
@@ -995,7 +996,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
Name: dl.Root.Label,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
})
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
+ _, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
if err != nil {
return nil, err
}
@@ -1005,7 +1006,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
Name: dl.Root.Label,
BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
})
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
+ _, err := renderPDF(ctx, ms, plugin, opts, inputPath, "", page, ruler, dl, doc, path, pageMap, includeNav)
if err != nil {
return nil, err
}
@@ -1021,7 +1022,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
return svg, nil
}
-func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
+func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, inputPath, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
var svg []byte
if !diagram.IsFolderOnly {
// gofpdf will print the png img with a slight filter
@@ -1054,7 +1055,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
l := simplelog.FromCmdLog(ms.Log)
- svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
+ svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil {
@@ -1119,7 +1120,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
@@ -1131,7 +1132,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
@@ -1143,7 +1144,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
BoardID: boardID,
LinkToSlide: boardIDToIndex[boardID] + 1,
})
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, inputPath, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
@@ -1275,7 +1276,7 @@ func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, p
return dictionary
}
-func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
+func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, inputPath string, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
if !diagram.IsFolderOnly {
var scale *float64
@@ -1301,7 +1302,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
l := simplelog.FromCmdLog(ms.Log)
- svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
+ svg, bundleErr := imgbundler.BundleLocal(ctx, l, inputPath, svg, cacheImages)
svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
bundleErr = multierr.Combine(bundleErr, bundleErr2)
if bundleErr != nil {
@@ -1318,21 +1319,21 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
}
for _, dl := range diagram.Layers {
- _, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
+ _, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, layerPNGs...)
}
for _, dl := range diagram.Scenarios {
- _, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
+ _, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
if err != nil {
return nil, nil, err
}
pngs = append(pngs, scenarioPNGs...)
}
for _, dl := range diagram.Steps {
- _, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
+ _, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, inputPath, dl)
if err != nil {
return nil, nil, err
}
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 5b314470f..dcf690903 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -69,6 +69,7 @@ func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
g := d2graph.NewGraph()
g.AST = ast
+ g.BaseAST = ast
c.compileBoard(g, m)
if len(c.err.Errors) > 0 {
return nil, c.err
@@ -90,6 +91,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
c.validateLabels(g)
c.validateNear(g)
c.validateEdges(g)
+ c.validatePositionsCompatibility(g)
c.compileBoardsField(g, ir, "layers")
c.compileBoardsField(g, ir, "scenarios")
@@ -121,7 +123,7 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
g2 := d2graph.NewGraph()
g2.Parent = g
g2.AST = f.Map().AST().(*d2ast.Map)
- g2.BaseAST = findFieldAST(g.AST, f)
+ g2.BaseAST = findFieldAST(g.BaseAST, f)
c.compileBoard(g2, f.Map())
g2.Name = f.Name
switch fieldName {
@@ -337,11 +339,11 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
}
if obj.Parent != nil {
- if obj.Parent.Shape.Value == d2target.ShapeSQLTable {
+ if strings.EqualFold(obj.Parent.Shape.Value, d2target.ShapeSQLTable) {
c.errorf(f.LastRef().AST(), "sql_table columns cannot have children")
return
}
- if obj.Parent.Shape.Value == d2target.ShapeClass {
+ if strings.EqualFold(obj.Parent.Shape.Value, d2target.ShapeClass) {
c.errorf(f.LastRef().AST(), "class fields cannot have children")
return
}
@@ -510,7 +512,7 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
return
}
attrs.Shape.Value = scalar.ScalarString()
- if attrs.Shape.Value == d2target.ShapeCode {
+ if strings.EqualFold(attrs.Shape.Value, d2target.ShapeCode) {
// Explicit code shape is plaintext.
attrs.Language = d2target.ShapeText
}
@@ -1008,12 +1010,12 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
}
}
if obj.Style.DoubleBorder != nil {
- if obj.Shape.Value != "" && obj.Shape.Value != d2target.ShapeSquare && obj.Shape.Value != d2target.ShapeRectangle && obj.Shape.Value != d2target.ShapeCircle && obj.Shape.Value != d2target.ShapeOval {
+ if obj.Shape.Value != "" && !strings.EqualFold(obj.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCircle) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeOval) {
c.errorf(obj.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
}
}
case "shape":
- if obj.Shape.Value == d2target.ShapeImage && obj.Icon == nil {
+ if strings.EqualFold(obj.Shape.Value, d2target.ShapeImage) && obj.Icon == nil {
c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
}
@@ -1023,14 +1025,14 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Shape.Value))
}
case "constraint":
- if obj.Shape.Value != d2target.ShapeSQLTable {
+ if !strings.EqualFold(obj.Shape.Value, d2target.ShapeSQLTable) {
c.errorf(f.LastPrimaryKey(), `"constraint" keyword can only be used in "sql_table" shapes`)
}
}
return
}
- if obj.Shape.Value == d2target.ShapeImage {
+ if strings.EqualFold(obj.Shape.Value, d2target.ShapeImage) {
c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
return
}
@@ -1043,7 +1045,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
func (c *compiler) validateLabels(g *d2graph.Graph) {
for _, obj := range g.Objects {
- if obj.Shape.Value != d2target.ShapeText {
+ if !strings.EqualFold(obj.Shape.Value, d2target.ShapeText) {
continue
}
if obj.Attributes.Language != "" {
@@ -1097,6 +1099,14 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
continue
}
}
+ if nearObj.ClosestGridDiagram() != nil {
+ c.errorf(obj.NearKey, "near keys cannot be set to descendants of special objects, like grid cells")
+ continue
+ }
+ if nearObj.OuterSequenceDiagram() != nil {
+ c.errorf(obj.NearKey, "near keys cannot be set to descendants of special objects, like sequence diagram actors")
+ continue
+ }
} else if isConst {
if obj.Parent != g.Root {
c.errorf(obj.NearKey, "constant near keys can only be set on root level shapes")
@@ -1122,6 +1132,26 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
+func (c *compiler) validatePositionsCompatibility(g *d2graph.Graph) {
+ for _, o := range g.Objects {
+ for _, pos := range []*d2graph.Scalar{o.Top, o.Left} {
+ if pos != nil {
+ if o.Parent != nil {
+ if strings.EqualFold(o.Parent.Shape.Value, d2target.ShapeHierarchy) {
+ c.errorf(pos.MapKey, `position keywords cannot be used with shape "hierarchy"`)
+ }
+ if o.OuterSequenceDiagram() != nil {
+ c.errorf(pos.MapKey, `position keywords cannot be used inside shape "sequence_diagram"`)
+ }
+ if o.Parent.GridColumns != nil || o.Parent.GridRows != nil {
+ c.errorf(pos.MapKey, `position keywords cannot be used with grids`)
+ }
+ }
+ }
+ }
+ }
+}
+
func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
// edges from a grid to something outside is ok
diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index c74398934..2350f1aba 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
+ "oss.terrastruct.com/util-go/mapfs"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
@@ -23,6 +24,8 @@ func TestCompile(t *testing.T) {
testCases := []struct {
name string
text string
+ // For tests that use imports, define `index.d2` as text and other files here
+ files map[string]string
expErr string
assertions func(t *testing.T, g *d2graph.Graph)
@@ -259,7 +262,7 @@ containers: {
}
}
`,
- expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain, paper`,
+ expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: none, dots, lines, grain, paper`,
},
{
name: "shape_unquoted_hex",
@@ -1607,6 +1610,17 @@ d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: 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: "near_special",
+
+ text: `x.near: z.x
+z: {
+ grid-rows: 1
+ x
+}
+`,
+ expErr: `d2/testdata/d2compiler/TestCompile/near_special.d2:1:9: near keys cannot be set to descendants of special objects, like grid cells`,
+ },
{
name: "near_bad_connected",
@@ -2837,15 +2851,96 @@ y.source-arrowhead.shape: cf-one
expErr: `d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:1:3: "target-arrowhead" can only be used on connections
d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowhead" can only be used on connections`,
},
+ {
+ name: "shape-hierarchy",
+ text: `x: {
+ shape: hierarchy
+ a -> b
+}
+`,
+ },
+ {
+ name: "fixed-pos-shape-hierarchy",
+ text: `x: {
+ shape: hierarchy
+ a -> b
+ a.top: 20
+ a.left: 20
+}
+`,
+ expErr: `d2/testdata/d2compiler/TestCompile/fixed-pos-shape-hierarchy.d2:4:2: position keywords cannot be used with shape "hierarchy"
+d2/testdata/d2compiler/TestCompile/fixed-pos-shape-hierarchy.d2:5:2: position keywords cannot be used with shape "hierarchy"`,
+ },
+ {
+ name: "vars-in-imports",
+ text: `dev: {
+ vars: {
+ env: Dev
+ }
+ ...@template.d2
+}
+
+qa: {
+ vars: {
+ env: Qa
+ }
+ ...@template.d2
+}
+`,
+ files: map[string]string{
+ "template.d2": `env: {
+ label: ${env} Environment
+ vm: {
+ label: My Virtual machine!
+ }
+}`,
+ },
+ assertions: func(t *testing.T, g *d2graph.Graph) {
+ tassert.Equal(t, "dev.env", g.Objects[1].AbsID())
+ tassert.Equal(t, "Dev Environment", g.Objects[1].Label.Value)
+ tassert.Equal(t, "qa.env", g.Objects[2].AbsID())
+ tassert.Equal(t, "Qa Environment", g.Objects[2].Label.Value)
+ },
+ },
+ {
+ name: "spread-import-link",
+ text: `k
+
+layers: {
+ x: {...@x}
+}`,
+ files: map[string]string{
+ "x.d2": `a.link: layers.b
+layers: {
+ b: {
+ d
+ }
+}`,
+ },
+ },
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
-
+ opts := &d2compiler.CompileOptions{}
+ if tc.files != nil {
+ tc.files["index.d2"] = tc.text
+ renamed := make(map[string]string)
+ for file, content := range tc.files {
+ renamed[fmt.Sprintf("d2/testdata/d2compiler/TestCompile/%v", file)] = content
+ }
+ fs, err := mapfs.New(renamed)
+ assert.Success(t, err)
+ t.Cleanup(func() {
+ err = fs.Close()
+ assert.Success(t, err)
+ })
+ opts.FS = fs
+ }
d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
- g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
+ g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), opts)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
@@ -2991,6 +3086,22 @@ steps: {
assert.True(t, g.IsFolderOnly)
},
},
+ {
+ name: "no-inherit-label",
+ run: func(t *testing.T) {
+ g, _ := assertCompile(t, `
+label: hi
+
+steps: {
+ 1: {
+ RJ
+ }
+}
+`, "")
+ assert.True(t, g.Root.Label.MapKey != nil)
+ assert.True(t, g.Steps[0].Root.Label.MapKey == nil)
+ },
+ },
{
name: "scenarios_edge_index",
run: func(t *testing.T) {
@@ -3239,6 +3350,17 @@ y: null
assert.Equal(t, 0, len(g.Edges))
},
},
+ {
+ name: "delete-nested-connection",
+ run: func(t *testing.T) {
+ g, _ := assertCompile(t, `
+a -> b.c
+b.c: null
+`, "")
+ assert.Equal(t, 2, len(g.Objects))
+ assert.Equal(t, 0, len(g.Edges))
+ },
+ },
{
name: "delete-multiple-connections",
run: func(t *testing.T) {
@@ -3424,6 +3546,24 @@ hi: "1 ${x} 2"
assert.Equal(t, "1 im a var 2", g.Objects[0].Label.Value)
},
},
+ {
+ name: "double-border",
+ run: func(t *testing.T) {
+ assertCompile(t, `
+a.shape: Circle
+a.style.double-border: true
+`, "")
+ },
+ },
+ {
+ name: "invalid-double-border",
+ run: func(t *testing.T) {
+ assertCompile(t, `
+a.shape: hexagon
+a.style.double-border: true
+`, `d2/testdata/d2compiler/TestCompile2/vars/basic/invalid-double-border.d2:3:1: key "double-border" can only be applied to squares, rectangles, circles, ovals`)
+ },
+ },
{
name: "single-quoted",
run: func(t *testing.T) {
@@ -4329,6 +4469,29 @@ container_2: {
assert.Equal(t, 4, len(g.Objects))
},
},
+ {
+ name: "override-edge/1",
+ run: func(t *testing.T) {
+ g, _ := assertCompile(t, `
+(* -> *)[*].style.stroke: red
+(* -> *)[*].style.stroke: green
+a -> b
+`, ``)
+ assert.Equal(t, "green", g.Edges[0].Attributes.Style.Stroke.Value)
+ },
+ },
+ {
+ name: "override-edge/2",
+ run: func(t *testing.T) {
+ g, _ := assertCompile(t, `
+(* -> *)[*].style.stroke: red
+a -> b: {style.stroke: green}
+a -> b
+`, ``)
+ assert.Equal(t, "green", g.Edges[0].Attributes.Style.Stroke.Value)
+ assert.Equal(t, "red", g.Edges[1].Attributes.Style.Stroke.Value)
+ },
+ },
}
for _, tc := range tca {
diff --git a/d2exporter/export.go b/d2exporter/export.go
index 6c25d5b8f..d01147af9 100644
--- a/d2exporter/export.go
+++ b/d2exporter/export.go
@@ -345,6 +345,9 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
if edge.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Style.Bold.Value)
}
+ if edge.Style.Underline != nil {
+ connection.Underline, _ = strconv.ParseBool(edge.Style.Underline.Value)
+ }
if theme != nil && theme.SpecialRules.Mono {
connection.FontFamily = "mono"
}
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index b70c887bd..da808ecfd 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -470,7 +470,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) {
+ 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 level == 1 {
if !obj.IsContainer() {
return color.B6
@@ -1048,15 +1048,6 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
// resizes the object to fit content of the given width and height in its inner box with the given padding.
// this accounts for the shape of the object, and if there is a desired width or height set for the object
func (obj *Object) SizeToContent(contentWidth, contentHeight, paddingX, paddingY float64) {
- var desiredWidth int
- var desiredHeight int
- if obj.WidthAttr != nil {
- desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
- }
- if obj.HeightAttr != nil {
- desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
- }
-
dslShape := strings.ToLower(obj.Shape.Value)
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
s := shape.NewShape(shapeType, geo.NewBox(geo.NewPoint(0, 0), contentWidth, contentHeight))
@@ -1068,8 +1059,28 @@ func (obj *Object) SizeToContent(contentWidth, contentHeight, paddingX, paddingY
} else {
fitWidth, fitHeight = s.GetDimensionsToFit(contentWidth, contentHeight, paddingX, paddingY)
}
- obj.Width = math.Max(float64(desiredWidth), fitWidth)
- obj.Height = math.Max(float64(desiredHeight), fitHeight)
+
+ var desiredWidth int
+ if obj.WidthAttr != nil {
+ desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
+ obj.Width = float64(desiredWidth)
+ } else {
+ obj.Width = fitWidth
+ }
+
+ var desiredHeight int
+ if obj.HeightAttr != nil {
+ desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
+ obj.Height = float64(desiredHeight)
+ } else {
+ obj.Height = fitHeight
+ }
+
+ if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
+ obj.Width = math.Max(float64(desiredWidth), fitWidth)
+ obj.Height = math.Max(float64(desiredHeight), fitHeight)
+ }
+
if s.AspectRatio1() {
sideLength := math.Max(obj.Width, obj.Height)
obj.Width = sideLength
@@ -1816,6 +1827,7 @@ var LabelPositionsMapping = map[string]label.Position{
}
var FillPatterns = []string{
+ "none",
"dots",
"lines",
"grain",
diff --git a/d2graph/serde.go b/d2graph/serde.go
index 88c3abd67..6b1006c31 100644
--- a/d2graph/serde.go
+++ b/d2graph/serde.go
@@ -28,7 +28,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
}
var root Object
- convert(sg.Root, &root)
+ Convert(sg.Root, &root)
g.Root = &root
root.Graph = g
g.RootLevel = sg.RootLevel
@@ -38,7 +38,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
var objects []*Object
for _, so := range sg.Objects {
var o Object
- if err := convert(so, &o); err != nil {
+ if err := Convert(so, &o); err != nil {
return err
}
o.Graph = g
@@ -67,7 +67,7 @@ func DeserializeGraph(bytes []byte, g *Graph) error {
var edges []*Edge
for _, se := range sg.Edges {
var e Edge
- if err := convert(se, &e); err != nil {
+ if err := Convert(se, &e); err != nil {
return err
}
@@ -108,7 +108,7 @@ func SerializeGraph(g *Graph) ([]byte, error) {
var sedges []SerializedEdge
for _, e := range g.Edges {
- se, err := toSerializedEdge(e)
+ se, err := ToSerializedEdge(e)
if err != nil {
return nil, err
}
@@ -121,7 +121,7 @@ func SerializeGraph(g *Graph) ([]byte, error) {
func toSerializedObject(o *Object) (SerializedObject, error) {
var so SerializedObject
- if err := convert(o, &so); err != nil {
+ if err := Convert(o, &so); err != nil {
return nil, err
}
@@ -138,9 +138,9 @@ func toSerializedObject(o *Object) (SerializedObject, error) {
return so, nil
}
-func toSerializedEdge(e *Edge) (SerializedEdge, error) {
+func ToSerializedEdge(e *Edge) (SerializedEdge, error) {
var se SerializedEdge
- if err := convert(e, &se); err != nil {
+ if err := Convert(e, &se); err != nil {
return nil, err
}
@@ -154,7 +154,7 @@ func toSerializedEdge(e *Edge) (SerializedEdge, error) {
return se, nil
}
-func convert[T, Q any](from T, to *Q) error {
+func Convert[T, Q any](from T, to *Q) error {
b, err := json.Marshal(from)
if err != nil {
return err
diff --git a/d2ir/compile.go b/d2ir/compile.go
index 37d3445a9..b05cda85c 100644
--- a/d2ir/compile.go
+++ b/d2ir/compile.go
@@ -31,8 +31,7 @@ type compiler struct {
imports []string
// importStack is used to detect cyclic imports.
importStack []string
- // importCache enables reuse of files imported multiple times.
- importCache map[string]*Map
+ seenImports map[string]struct{}
utf16Pos bool
// Stack of globs that must be recomputed at each new object in and below the current scope.
@@ -62,7 +61,7 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
err: &d2parser.ParseError{},
fs: opts.FS,
- importCache: make(map[string]*Map),
+ seenImports: make(map[string]struct{}),
utf16Pos: opts.UTF16Pos,
}
m := &Map{}
@@ -127,14 +126,21 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) {
varsStack = append([]*Map{f.Map()}, varsStack...)
}
}
- for _, f := range m.Fields {
+ for i := 0; i < len(m.Fields); i++ {
+ f := m.Fields[i]
if f.Primary() != nil {
- c.resolveSubstitutions(varsStack, f)
+ removed := c.resolveSubstitutions(varsStack, f)
+ if removed {
+ i--
+ }
}
if arr, ok := f.Composite.(*Array); ok {
for _, val := range arr.Values {
if scalar, ok := val.(*Scalar); ok {
- c.resolveSubstitutions(varsStack, scalar)
+ removed := c.resolveSubstitutions(varsStack, scalar)
+ if removed {
+ i--
+ }
}
}
} else if f.Map() != nil {
@@ -213,7 +219,7 @@ func (c *compiler) validateConfigs(configs *Field) {
}
}
-func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) {
+func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedField bool) {
var subbed bool
var resolvedField *Field
@@ -264,6 +270,7 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) {
for i, f2 := range m.Fields {
if n == f2 {
m.Fields = append(m.Fields[:i], m.Fields[i+1:]...)
+ removedField = true
break
}
}
@@ -334,6 +341,7 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) {
s.Coalesce()
}
}
+ return removedField
}
func (c *compiler) resolveSubstitution(vars *Map, substitution *d2ast.Substitution) *Field {
@@ -361,6 +369,9 @@ func (c *compiler) overlay(base *Map, f *Field) {
return
}
base = base.CopyBase(f)
+ // Certain fields should never carry forward.
+ // If you give your scenario a label, you don't want all steps in a scenario to be labeled the same.
+ base.DeleteField("label")
OverlayMap(base, f.Map())
f.Composite = base
}
@@ -381,6 +392,9 @@ func (g *globContext) prefixed(dst *Map) *globContext {
if len(prefix.Path) > 0 {
g2.refctx.Key.Key = prefix
}
+ if !g2.refctx.Key.HasTripleGlob() && g2.refctx.Key.EdgeKey != nil {
+ prefix.Path = append(prefix.Path, g2.refctx.Key.EdgeKey.Path...)
+ }
return g2
}
@@ -410,6 +424,7 @@ func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
}
delete(gctx.appliedFields, ks)
+ delete(gctx.appliedEdges, ks)
return false
}
}
@@ -497,6 +512,7 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
}
OverlayMap(dst, impn.Map())
+ c.updateLinks(dst)
if impnf, ok := impn.(*Field); ok {
if impnf.Primary_ != nil {
@@ -979,7 +995,7 @@ func (c *compiler) compileEdges(refctx *RefContext) {
func (c *compiler) _compileEdges(refctx *RefContext) {
eida := NewEdgeIDs(refctx.Key)
for i, eid := range eida {
- if refctx.Key != nil && refctx.Key.Value.Null != nil {
+ if !eid.Glob && (refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil) {
refctx.ScopeMap.DeleteEdge(eid)
continue
}
@@ -997,6 +1013,10 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
continue
}
for _, e := range ea {
+ if refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil {
+ refctx.ScopeMap.DeleteEdge(e.ID)
+ continue
+ }
e.References = append(e.References, &EdgeReference{
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go
index 5a21c549e..1ab518acc 100644
--- a/d2ir/d2ir.go
+++ b/d2ir/d2ir.go
@@ -950,13 +950,20 @@ func (m *Map) DeleteField(ida ...string) *Field {
}
if len(rest) == 0 {
for _, fr := range f.References {
- for _, e := range m.Edges {
- for _, er := range e.References {
- if er.Context_ == fr.Context_ {
- m.DeleteEdge(e.ID)
- break
+ currM := m
+ for currM != nil {
+ for _, e := range currM.Edges {
+ for _, er := range e.References {
+ if er.Context_ == fr.Context_ {
+ currM.DeleteEdge(e.ID)
+ break
+ }
}
}
+ if NodeBoardKind(currM) != "" {
+ break
+ }
+ currM = ParentMap(currM)
}
}
m.Fields = append(m.Fields[:i], m.Fields[i+1:]...)
@@ -1087,7 +1094,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[
}
gctx.appliedEdges[ks] = struct{}{}
}
- *ea = append(*ea, ea2...)
+ *ea = append(*ea, e)
}
}
}
@@ -1348,11 +1355,8 @@ func (m *Map) AST() d2ast.Node {
if m == nil {
return nil
}
- astMap := &d2ast.Map{}
- if m.Root() {
- astMap.Range = d2ast.MakeRange(",0:0:0-1:0:0")
- } else {
- astMap.Range = d2ast.MakeRange(",1:0:0-2:0:0")
+ astMap := &d2ast.Map{
+ Range: d2ast.MakeRange(",0:0:0-1:0:0"),
}
for _, f := range m.Fields {
astMap.Nodes = append(astMap.Nodes, d2ast.MakeMapNodeBox(f.AST().(d2ast.MapNode)))
diff --git a/d2ir/filter_test.go b/d2ir/filter_test.go
index 63375b816..06e926416 100644
--- a/d2ir/filter_test.go
+++ b/d2ir/filter_test.go
@@ -150,6 +150,21 @@ x -> y
assertQuery(t, m, 0, 0, 0.1, "(x -> y)[1].style.opacity")
},
},
+ {
+ name: "label-filter/3",
+ run: func(t testing.TB) {
+ m, err := compile(t, `
+(* -> *)[*]: {
+ &label: hi
+ style.opacity: 0.1
+}
+
+x -> y: hi
+`)
+ assert.Success(t, err)
+ assertQuery(t, m, 0, 0, 0.1, "(x -> y)[0].style.opacity")
+ },
+ },
{
name: "lazy-filter",
run: func(t testing.TB) {
diff --git a/d2ir/import.go b/d2ir/import.go
index b933298dd..c415cf138 100644
--- a/d2ir/import.go
+++ b/d2ir/import.go
@@ -82,16 +82,11 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
// Only get immediate imports.
if len(c.importStack) == 2 {
- if _, ok := c.importCache[impPath]; !ok {
+ if _, ok := c.seenImports[impPath]; !ok {
c.imports = append(c.imports, imp.PathWithPre())
}
}
- ir, ok := c.importCache[impPath]
- if ok {
- return ir, true
- }
-
var f fs.File
var err error
if c.fs == nil {
@@ -113,13 +108,13 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) {
return nil, false
}
- ir = &Map{}
+ ir := &Map{}
ir.initRoot()
ir.parent.(*Field).References[0].Context_.Scope = ast
c.compileMap(ir, ast, ast)
- c.importCache[impPath] = ir
+ c.seenImports[impPath] = struct{}{}
return ir, true
}
diff --git a/d2ir/import_test.go b/d2ir/import_test.go
index 81a3e3f50..d67f5950d 100644
--- a/d2ir/import_test.go
+++ b/d2ir/import_test.go
@@ -232,7 +232,7 @@ label: meow`,
_, err := compileFS(t, "index.d2", map[string]string{
"index.d2": "...@'./../x.d2'",
})
- assert.ErrorString(t, err, `index.d2:1:1: failed to import "../x.d2": stat ../x.d2: invalid argument`)
+ assert.ErrorString(t, err, `index.d2:1:1: failed to import "../x.d2": open ../x.d2: invalid argument`)
},
},
{
diff --git a/d2ir/pattern_test.go b/d2ir/pattern_test.go
index 941db0785..8cc32c6cf 100644
--- a/d2ir/pattern_test.go
+++ b/d2ir/pattern_test.go
@@ -310,6 +310,79 @@ layers.x: { wrapper.p }
assertQuery(t, m, 0, 0, nil, "layers.x.wrapper.p")
},
},
+ {
+ name: "edge-glob-null",
+ run: func(t testing.TB) {
+ m, err := compile(t, `a -> b
+(* -> *)[*]: null
+x -> y
+`)
+ assert.Success(t, err)
+ // 4 fields and 0 edges
+ assertQuery(t, m, 4, 0, nil, "")
+ },
+ },
+ {
+ name: "field-glob-style-inherit",
+ run: func(t testing.TB) {
+ m, err := compile(t, `*.style.opacity: 0
+x: {
+ style.opacity: 1
+}
+
+scenarios: {
+ 1: {
+ x
+ }
+}
+`)
+ assert.Success(t, err)
+ assertQuery(t, m, 0, 0, 1, "x.style.opacity")
+ assertQuery(t, m, 0, 0, 1, "scenarios.1.x.style.opacity")
+ },
+ },
+ {
+ name: "edge-glob-style-inherit/1",
+ run: func(t testing.TB) {
+ m, err := compile(t, `(* -> *)[*].style.opacity: 0
+x -> y: {
+ style.opacity: 1
+}
+
+scenarios: {
+ 1: {
+ x
+ }
+}
+`)
+ assert.Success(t, err)
+ assertQuery(t, m, 0, 0, 1, "(x -> y)[0].style.opacity")
+ assertQuery(t, m, 0, 0, 1, "scenarios.1.(x -> y)[0].style.opacity")
+ },
+ },
+ {
+ name: "edge-glob-style-inherit/2",
+ run: func(t testing.TB) {
+ m, err := compile(t, `*.style.opacity: 0
+(* -> *)[*].style.opacity: 0
+x -> y
+
+steps: {
+ 1: {
+ x.style.opacity: 1
+ }
+ 2: {
+ (x -> y)[0].style.opacity: 1
+ }
+ 3: {
+ y.style.opacity: 1
+ }
+}
+`)
+ assert.Success(t, err)
+ assertQuery(t, m, 0, 0, 1, "steps.3.(x -> y)[0].style.opacity")
+ },
+ },
{
name: "double-glob/edge/1",
run: func(t testing.TB) {
diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go
index b367e404c..59c477403 100644
--- a/d2layouts/d2dagrelayout/layout.go
+++ b/d2layouts/d2dagrelayout/layout.go
@@ -570,6 +570,13 @@ func positionLabelsIcons(obj *d2graph.Object) {
} 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/d2elklayout/layout.go b/d2layouts/d2elklayout/layout.go
index 2c0b45a9f..6d4c335f1 100644
--- a/d2layouts/d2elklayout/layout.go
+++ b/d2layouts/d2elklayout/layout.go
@@ -1148,5 +1148,12 @@ func positionLabelsIcons(obj *d2graph.Object) {
} 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/d2oracle/edit.go b/d2oracle/edit.go
index 076e5d6ea..cc84983d9 100644
--- a/d2oracle/edit.go
+++ b/d2oracle/edit.go
@@ -199,7 +199,7 @@ func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey,
refs := edge.References
if baseAST != g.AST {
- refs = getWriteableEdgeRefs(edge, baseAST)
+ refs = GetWriteableEdgeRefs(edge, baseAST)
if len(refs) == 0 || refs[0].ScopeAST != baseAST {
// TODO null
return nil, OutsideScopeError{}
@@ -383,11 +383,11 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
break
}
obj = o
- imported = IsImported(baseAST, obj)
+ imported = IsImportedObj(baseAST, obj)
var maybeNewScope *d2ast.Map
if baseAST != g.AST || imported {
- writeableRefs := getWriteableRefs(obj, baseAST)
+ writeableRefs := GetWriteableRefs(obj, baseAST)
for _, ref := range writeableRefs {
if ref.MapKey != nil && ref.MapKey.Value.Map != nil {
maybeNewScope = ref.MapKey.Value.Map
@@ -414,7 +414,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
writeableLabelMK := true
var objK *d2ast.Key
if baseAST != g.AST || imported {
- writeableRefs := getWriteableRefs(obj, baseAST)
+ writeableRefs := GetWriteableRefs(obj, baseAST)
if len(writeableRefs) > 0 {
objK = writeableRefs[0].MapKey
}
@@ -429,6 +429,19 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
break
}
}
+ } else {
+ // Even if not imported or different board, a label can be not writeable if it's in a class or var or glob
+ // In those cases, the label is not a direct object reference
+ found := false
+ for _, ref := range obj.References {
+ if ref.MapKey == obj.Label.MapKey {
+ found = true
+ break
+ }
+ }
+ if !found {
+ writeableLabelMK = false
+ }
}
var m *d2ast.Map
if objK != nil {
@@ -481,12 +494,17 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
if !ok {
return errors.New("edge not found")
}
+ imported = IsImportedEdge(baseAST, edge)
refs := edge.References
- if baseAST != g.AST {
- refs = getWriteableEdgeRefs(edge, baseAST)
+ if baseAST != g.AST || imported {
+ refs = GetWriteableEdgeRefs(edge, baseAST)
}
onlyInChain := true
- for _, ref := range refs {
+ var earliestRef *d2graph.EdgeReference
+ for i, ref := range refs {
+ if earliestRef == nil || ref.MapKey.Range.Before(earliestRef.MapKey.Range) {
+ earliestRef = &refs[i]
+ }
// TODO merge flat edgekeys
// E.g. this can group into a map
// (y -> z)[0].style.opacity: 0.4
@@ -496,20 +514,31 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
onlyInChain = false
}
}
- // If a ref has an exact match on this key, just change the value
- tmp1 := *ref.MapKey
- tmp2 := *mk
- noVal1 := &tmp1
- noVal2 := &tmp2
- noVal1.Value = d2ast.ValueBox{}
- noVal2.Value = d2ast.ValueBox{}
- if noVal1.D2OracleEquals(noVal2) {
- ref.MapKey.Value = mk.Value
- return nil
+
+ if ref.MapKey.EdgeIndex == nil || !ref.MapKey.EdgeIndex.Glob {
+ // If a ref has an exact match on this key, just change the value
+ tmp1 := *ref.MapKey
+ tmp2 := *mk
+ noVal1 := &tmp1
+ noVal2 := &tmp2
+ noVal1.Value = d2ast.ValueBox{}
+ noVal2.Value = d2ast.ValueBox{}
+ if noVal1.D2OracleEquals(noVal2) {
+ ref.MapKey.Value = mk.Value
+ return nil
+ }
}
}
if onlyInChain {
- appendMapKey(scope, mk)
+ if earliestRef != nil && scope.Range.Before(earliestRef.MapKey.Range) {
+ // Since the original mk was trimmed to common, we set to the edge that
+ // the ref's scope is in
+ mk.Edges[0] = earliestRef.Edge
+ // We can't reference an edge before it's been defined
+ earliestRef.Scope.InsertAfter(earliestRef.MapKey, mk)
+ } else {
+ appendMapKey(scope, mk)
+ }
return nil
}
attrs = edge.Attributes
@@ -533,7 +562,7 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
foundMap = true
scope = ref.MapKey.Value.Map
for _, n := range scope.Nodes {
- if n.MapKey.Value.Map == nil {
+ if n.MapKey == nil || n.MapKey.Value.Map == nil {
continue
}
if n.MapKey.Key == nil || len(n.MapKey.Key.Path) != 1 {
@@ -574,6 +603,10 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
if s.MapKey.Range.Path != baseAST.Range.Path {
return false
}
+ // Globs are also not writeable
+ if s.MapKey.HasGlob() {
+ return false
+ }
}
return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
}
@@ -646,14 +679,20 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
case "source-arrowhead", "target-arrowhead":
var arrowhead *d2graph.Attributes
if reservedKey == "source-arrowhead" {
+ if edge.SrcArrowhead != nil {
+ attrs = *edge.SrcArrowhead
+ }
arrowhead = edge.SrcArrowhead
} else {
+ if edge.DstArrowhead != nil {
+ attrs = *edge.DstArrowhead
+ }
arrowhead = edge.DstArrowhead
}
if arrowhead != nil {
if reservedTargetKey == "" {
- if len(mk.Key.Path[reservedIndex:]) != 2 {
- return errors.New("malformed style setting, expected 2 part path")
+ if len(mk.Key.Path[reservedIndex:]) < 2 {
+ return errors.New("malformed style setting, expected >= 2 part path")
}
reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
}
@@ -668,6 +707,12 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
arrowhead.Label.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
+ case "style":
+ reservedTargetKey = mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString()
+ if inlined(attrs.Style.Filled) {
+ attrs.Style.Filled.MapKey.SetScalar(mk.Value.ScalarBox())
+ return nil
+ }
}
}
case "style":
@@ -770,9 +815,20 @@ func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string)
}
}
case "label":
- if inlined(&attrs.Label) {
- attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
- return nil
+ if len(mk.Key.Path[reservedIndex:]) > 1 {
+ reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
+ switch reservedTargetKey {
+ case "near":
+ if inlined(attrs.LabelPosition) {
+ attrs.LabelPosition.MapKey.SetScalar(mk.Value.ScalarBox())
+ return nil
+ }
+ }
+ } else {
+ if inlined(&attrs.Label) {
+ attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
+ return nil
+ }
}
}
}
@@ -846,7 +902,7 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
baseAST = boardG.BaseAST
}
- g2, err := deleteReserved(g, mk)
+ g2, err := deleteReserved(g, boardPath, baseAST, mk)
if err != nil {
return nil, err
}
@@ -868,45 +924,55 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
return g, nil
}
- refs := e.References
- if len(boardPath) > 0 {
- refs := getWriteableEdgeRefs(e, baseAST)
- if len(refs) != len(e.References) {
- mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
- }
- }
+ imported := IsImportedEdge(baseAST, e)
- if _, ok := mk.Value.Unbox().(*d2ast.Null); !ok {
- ref := refs[0]
- var refEdges []*d2ast.Edge
- for _, ref := range refs {
- refEdges = append(refEdges, ref.Edge)
- }
- ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Src, true)
- ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Dst, false)
-
- for i := len(e.References) - 1; i >= 0; i-- {
- ref := e.References[i]
- deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
+ if imported {
+ mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
+ appendMapKey(baseAST, mk)
+ } else {
+ refs := e.References
+ if len(boardPath) > 0 {
+ refs := GetWriteableEdgeRefs(e, baseAST)
+ if len(refs) != len(e.References) {
+ mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
+ }
}
- edges, ok := obj.FindEdges(mk)
- if ok {
- for _, e2 := range edges {
- if e2.Index <= e.Index {
- continue
+ if _, ok := mk.Value.Unbox().(*d2ast.Null); !ok {
+ ref := refs[0]
+ var refEdges []*d2ast.Edge
+ for _, ref := range refs {
+ refEdges = append(refEdges, ref.Edge)
+ }
+ ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Src, true)
+ ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Dst, false)
+
+ for i := len(e.References) - 1; i >= 0; i-- {
+ ref := e.References[i]
+ // Leave glob setters alone
+ if !(ref.MapKey.EdgeIndex != nil && ref.MapKey.EdgeIndex.Glob) {
+ deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
}
- for i := len(e2.References) - 1; i >= 0; i-- {
- ref := e2.References[i]
- if ref.MapKey.EdgeIndex != nil {
- *ref.MapKey.EdgeIndex.Int--
+ }
+
+ edges, ok := obj.FindEdges(mk)
+ if ok {
+ for _, e2 := range edges {
+ if e2.Index <= e.Index {
+ continue
+ }
+ for i := len(e2.References) - 1; i >= 0; i-- {
+ ref := e2.References[i]
+ if ref.MapKey.EdgeIndex != nil {
+ *ref.MapKey.EdgeIndex.Int--
+ }
}
}
}
+ } else {
+ // NOTE: it only needs to be after the last ref, but perhaps simplest and cleanest to append all nulls at the end
+ appendMapKey(baseAST, mk)
}
- } else {
- // NOTE: it only needs to be after the last ref, but perhaps simplest and cleanest to append all nulls at the end
- appendMapKey(baseAST, mk)
}
if len(boardPath) > 0 {
replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
@@ -918,17 +984,19 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
return recompile(boardG)
}
- prevG, _ := recompile(boardG)
+ prevG, err := recompile(boardG)
+ if err != nil {
+ return nil, err
+ }
obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return g, nil
}
- imported := IsImported(baseAST, obj)
+ imported := IsImportedObj(baseAST, obj)
if imported {
- println(d2format.Format(boardG.AST))
mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
appendMapKey(baseAST, mk)
} else {
@@ -941,7 +1009,7 @@ func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph,
return g, nil
}
if len(boardPath) > 0 {
- writeableRefs := getWriteableRefs(obj, baseAST)
+ writeableRefs := GetWriteableRefs(obj, baseAST)
if len(writeableRefs) != len(obj.References) {
mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
}
@@ -997,7 +1065,7 @@ func bumpChildrenUnderscores(m *d2ast.Map) {
}
func hoistRefChildren(g *d2graph.Graph, key *d2ast.KeyPath, ref d2graph.Reference) {
- if ref.MapKey.Value.Map == nil {
+ if ref.MapKey == nil || ref.MapKey.Value.Map == nil {
return
}
@@ -1053,7 +1121,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
var absKeys []*d2ast.KeyPath
if len(ref.Key.Path)-1 == ref.KeyPathIndex {
- if ref.MapKey.Value.Map == nil {
+ if ref.MapKey == nil || ref.MapKey.Value.Map == nil {
continue
}
var mapKeys []*d2ast.KeyPath
@@ -1178,7 +1246,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
return g, nil
}
-func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
+func deleteReserved(g *d2graph.Graph, boardPath []string, baseAST *d2ast.Map, mk *d2ast.Key) (*d2graph.Graph, error) {
targetKey := mk.Key
if len(mk.Edges) == 1 {
if mk.EdgeKey == nil {
@@ -1193,10 +1261,17 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
var e *d2graph.Edge
obj := g.Root
+ if len(boardPath) > 0 {
+ boardG := GetBoardGraph(g, boardPath)
+ if boardG == nil {
+ return nil, fmt.Errorf("board %v not found", boardPath)
+ }
+ obj = boardG.Root
+ }
if len(mk.Edges) == 1 {
if mk.Key != nil {
var ok bool
- obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
+ obj, ok = obj.HasChild(d2graph.Key(mk.Key))
if !ok {
return g, nil
}
@@ -1205,26 +1280,45 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
if !ok {
return g, nil
}
+ imported := IsImportedEdge(baseAST, e)
- if err := deleteEdgeField(g, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()); err != nil {
+ deleted, err := deleteEdgeField(g, baseAST, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString())
+ if err != nil {
return nil, err
}
+ if !deleted && imported {
+ mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
+ appendMapKey(baseAST, mk)
+ }
return recompile(g)
}
- isStyleKey := false
- for _, id := range d2graph.Key(targetKey) {
+ isNestedKey := false
+ imported := false
+ parts := d2graph.Key(targetKey)
+ for i, id := range parts {
_, ok := d2graph.ReservedKeywords[id]
if ok {
if id == "style" {
- isStyleKey = true
+ isNestedKey = true
continue
}
- if isStyleKey {
- err := deleteObjField(g, obj, id)
+ if id == "label" || id == "icon" {
+ if i < len(parts)-1 {
+ isNestedKey = true
+ continue
+ }
+ }
+ if isNestedKey {
+ deleted, err := deleteObjField(g, baseAST, obj, id)
if err != nil {
return nil, err
}
+ if !deleted && imported {
+ mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
+ appendMapKey(baseAST, mk)
+ }
+ continue
}
if id == "near" ||
@@ -1235,10 +1329,15 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
id == "left" ||
id == "top" ||
id == "link" {
- err := deleteObjField(g, obj, id)
+ deleted, err := deleteObjField(g, baseAST, obj, id)
if err != nil {
return nil, err
}
+ if !deleted && imported {
+ mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
+ appendMapKey(baseAST, mk)
+ } else {
+ }
}
break
}
@@ -1246,59 +1345,84 @@ func deleteReserved(g *d2graph.Graph, mk *d2ast.Key) (*d2graph.Graph, error) {
if !ok {
return nil, fmt.Errorf("object not found")
}
+ imported = IsImportedObj(baseAST, obj)
}
return recompile(g)
}
-func deleteMapField(m *d2ast.Map, field string) {
+func deleteMapField(m *d2ast.Map, field string) (deleted bool) {
for i := 0; i < len(m.Nodes); i++ {
n := m.Nodes[i]
if n.MapKey != nil && n.MapKey.Key != nil {
if n.MapKey.Key.Path[0].Unbox().ScalarString() == field {
deleteFromMap(m, n.MapKey)
} else if n.MapKey.Key.Path[0].Unbox().ScalarString() == "style" ||
+ n.MapKey.Key.Path[0].Unbox().ScalarString() == "label" ||
+ n.MapKey.Key.Path[0].Unbox().ScalarString() == "icon" ||
n.MapKey.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" ||
n.MapKey.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" {
if n.MapKey.Value.Map != nil {
- deleteMapField(n.MapKey.Value.Map, field)
+ deleted2 := deleteMapField(n.MapKey.Value.Map, field)
+ if deleted2 {
+ deleted = true
+ }
if len(n.MapKey.Value.Map.Nodes) == 0 {
- deleteFromMap(m, n.MapKey)
+ deleted2 := deleteFromMap(m, n.MapKey)
+ if deleted2 {
+ deleted = true
+ }
}
} else if len(n.MapKey.Key.Path) == 2 && n.MapKey.Key.Path[1].Unbox().ScalarString() == field {
- deleteFromMap(m, n.MapKey)
+ deleted2 := deleteFromMap(m, n.MapKey)
+ if deleted2 {
+ deleted = true
+ }
}
}
}
}
+ return deleted
}
-func deleteEdgeField(g *d2graph.Graph, e *d2graph.Edge, field string) error {
+func deleteEdgeField(g *d2graph.Graph, ast *d2ast.Map, e *d2graph.Edge, field string) (deleted bool, _ error) {
for _, ref := range e.References {
// Edge chains can't have fields
if len(ref.MapKey.Edges) > 1 {
continue
}
+ if ref.MapKey.Range.Path != ast.Range.Path {
+ continue
+ }
if ref.MapKey.Value.Map != nil {
- deleteMapField(ref.MapKey.Value.Map, field)
+ deleted2 := deleteMapField(ref.MapKey.Value.Map, field)
+ if deleted2 {
+ deleted = true
+ }
} else if ref.MapKey.EdgeKey != nil && ref.MapKey.EdgeKey.Path[len(ref.MapKey.EdgeKey.Path)-1].Unbox().ScalarString() == field {
// It's always safe to delete, since edge references must coexist with edge definition elsewhere
- deleteFromMap(ref.Scope, ref.MapKey)
+ deleted2 := deleteFromMap(ref.Scope, ref.MapKey)
+ if deleted2 {
+ deleted = true
+ }
}
}
- return nil
+ return deleted, nil
}
-func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
+func deleteObjField(g *d2graph.Graph, ast *d2ast.Map, obj *d2graph.Object, field string) (deleted bool, _ error) {
objK, err := d2parser.ParseKey(obj.AbsID())
if err != nil {
- return err
+ return false, err
}
objGK := d2graph.Key(objK)
for _, ref := range obj.References {
if ref.InEdge() {
continue
}
+ if ref.Key.Range.Path != ast.Range.Path {
+ continue
+ }
if ref.MapKey.Value.Map != nil {
deleteMapField(ref.MapKey.Value.Map, field)
} else if (len(ref.Key.Path) >= 2 &&
@@ -1306,15 +1430,20 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == obj.ID) ||
(len(ref.Key.Path) >= 3 &&
ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString() == field &&
- ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "style" &&
+ (ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "style" ||
+ ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "label" ||
+ ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "icon") &&
ref.Key.Path[len(ref.Key.Path)-3].Unbox().ScalarString() == obj.ID) {
tmpNodes := make([]d2ast.MapNodeBox, len(ref.Scope.Nodes))
copy(tmpNodes, ref.Scope.Nodes)
// If I delete this, will the object still exist?
- deleteFromMap(ref.Scope, ref.MapKey)
+ deleted2 := deleteFromMap(ref.Scope, ref.MapKey)
+ if deleted2 {
+ deleted = true
+ }
g2, err := recompile(g)
if err != nil {
- return err
+ return false, err
}
if _, ok := g2.Root.HasChild(objGK); !ok {
// Nope, so can't delete it, just remove the field then
@@ -1325,7 +1454,7 @@ func deleteObjField(g *d2graph.Graph, obj *d2graph.Object, field string) error {
}
}
- return nil
+ return deleted, nil
}
func deleteObject(g *d2graph.Graph, baseAST *d2ast.Map, key *d2ast.KeyPath, obj *d2graph.Object) (*d2graph.Graph, error) {
@@ -1635,7 +1764,10 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
return recompile(g)
}
- prevG, _ := recompile(boardG)
+ prevG, err := recompile(boardG)
+ if err != nil {
+ return nil, err
+ }
ak := d2graph.Key(mk.Key)
ak2 := d2graph.Key(mk2.Key)
@@ -1655,7 +1787,7 @@ func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDesce
}
if len(boardPath) > 0 {
- writeableRefs := getWriteableRefs(obj, baseAST)
+ writeableRefs := GetWriteableRefs(obj, baseAST)
if len(writeableRefs) != len(obj.References) {
return nil, OutsideScopeError{}
}
@@ -2159,8 +2291,17 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bo
if len(n.MapKey.Key.Path) == 0 {
continue
}
+ if len(n.MapKey.Key.Path) > 1 {
+ if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "label" ||
+ n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "icon" {
+ continue
+ }
+ }
if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
k := n.MapKey.Value.ScalarBox().Unbox().ScalarString()
+ if _, ok := d2graph.NearConstants[k]; ok {
+ continue
+ }
if strings.EqualFold(k, *from) && to == nil {
deleteFromMap(obj.Map, n.MapKey)
} else {
@@ -3122,21 +3263,3 @@ func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
}
return
}
-
-func getWriteableRefs(obj *d2graph.Object, writeableAST *d2ast.Map) (out []d2graph.Reference) {
- for i, ref := range obj.References {
- if ref.ScopeAST == writeableAST && ref.Key.Range.Path == writeableAST.Range.Path {
- out = append(out, obj.References[i])
- }
- }
- return
-}
-
-func getWriteableEdgeRefs(edge *d2graph.Edge, writeableAST *d2ast.Map) (out []d2graph.EdgeReference) {
- for i, ref := range edge.References {
- if ref.ScopeAST == writeableAST {
- out = append(out, edge.References[i])
- }
- }
- return
-}
diff --git a/d2oracle/edit_test.go b/d2oracle/edit_test.go
index 198cc83dc..bbab7f2e8 100644
--- a/d2oracle/edit_test.go
+++ b/d2oracle/edit_test.go
@@ -1177,6 +1177,83 @@ b
}
}
b: {style.fill: green}
+`,
+ },
+ {
+ name: "class-with-label",
+ text: `classes: {
+ user: {
+ label: ""
+ }
+}
+
+a.class: user
+`,
+ key: `a.style.opacity`,
+ value: go2.Pointer(`0.5`),
+ exp: `classes: {
+ user: {
+ label: ""
+ }
+}
+
+a.class: user
+a.style.opacity: 0.5
+`,
+ },
+ {
+ name: "edge-class-with-label",
+ text: `classes: {
+ user: {
+ label: ""
+ }
+}
+
+a -> b: {
+ class: user
+}
+`,
+ key: `(a -> b)[0].style.opacity`,
+ value: go2.Pointer(`0.5`),
+ exp: `classes: {
+ user: {
+ label: ""
+ }
+}
+
+a -> b: {
+ class: user
+ style.opacity: 0.5
+}
+`,
+ },
+ {
+ name: "var-with-label",
+ text: `vars: {
+ user: ""
+}
+
+a: ${user}
+`,
+ key: `a.style.opacity`,
+ value: go2.Pointer(`0.5`),
+ exp: `vars: {
+ user: ""
+}
+
+a: ${user} {style.opacity: 0.5}
+`,
+ },
+ {
+ name: "glob-with-label",
+ text: `*.label: ""
+a
+`,
+ key: `a.style.opacity`,
+ value: go2.Pointer(`0.5`),
+ exp: `*.label: ""
+a
+a.style.opacity: 0.5
`,
},
{
@@ -1518,6 +1595,86 @@ a.b -> a.c: {style.animated: true}
value: go2.Pointer(`diamond`),
exp: `x -> y: {target-arrowhead.shape: diamond}
+`,
+ },
+ {
+ name: "edge-arrowhead-filled/1",
+ text: `x -> y
+`,
+ key: `(x -> y)[0].target-arrowhead.style.filled`,
+ value: go2.Pointer(`true`),
+
+ exp: `x -> y: {target-arrowhead.style.filled: true}
+`,
+ },
+ {
+ name: "edge-arrowhead-filled/2",
+ text: `x -> y: {
+ target-arrowhead: * {
+ shape: diamond
+ }
+}
+`,
+ key: `(x -> y)[0].target-arrowhead.style.filled`,
+ value: go2.Pointer(`true`),
+
+ exp: `x -> y: {
+ target-arrowhead: * {
+ shape: diamond
+ style.filled: true
+ }
+}
+`,
+ },
+ {
+ name: "edge-arrowhead-filled/3",
+ text: `x -> y: {
+ target-arrowhead.shape: diamond
+}
+`,
+ key: `(x -> y)[0].target-arrowhead.style.filled`,
+ value: go2.Pointer(`true`),
+
+ exp: `x -> y: {
+ target-arrowhead.shape: diamond
+ target-arrowhead.style.filled: true
+}
+`,
+ },
+ {
+ name: "edge-arrowhead-filled/4",
+ text: `x -> y: {
+ target-arrowhead.shape: diamond
+ target-arrowhead.style.filled: true
+}
+`,
+ key: `(x -> y)[0].target-arrowhead.style.filled`,
+ value: go2.Pointer(`false`),
+
+ exp: `x -> y: {
+ target-arrowhead.shape: diamond
+ target-arrowhead.style.filled: false
+}
+`,
+ },
+ {
+ name: "edge-arrowhead-filled/5",
+ text: `x -> y: {
+ target-arrowhead.shape: diamond
+ target-arrowhead.style: {
+ filled: false
+ }
+}
+`,
+ key: `(x -> y)[0].target-arrowhead.style.filled`,
+ value: go2.Pointer(`true`),
+
+ exp: `x -> y: {
+ target-arrowhead.shape: diamond
+ target-arrowhead.style: {
+ filled: true
+ }
+}
`,
},
{
@@ -2171,6 +2328,252 @@ layers: {
b.style.fill: red
}
}
+`,
+ },
+ {
+ name: "import/9",
+
+ text: `...@yo
+`,
+ fsTexts: map[string]string{
+ "yo.d2": `a -> b`,
+ },
+ key: `(a -> b)[0].style.stroke`,
+ value: go2.Pointer(`red`),
+ exp: `...@yo
+(a -> b)[0].style.stroke: red
+`,
+ },
+ {
+ name: "label-near/1",
+
+ text: `x
+`,
+ key: `x.label.near`,
+ value: go2.Pointer(`bottom-right`),
+ exp: `x: {label.near: bottom-right}
+`,
+ },
+ {
+ name: "label-near/2",
+
+ text: `x.label.near: bottom-left
+`,
+ key: `x.label.near`,
+ value: go2.Pointer(`bottom-right`),
+ exp: `x.label.near: bottom-right
+`,
+ },
+ {
+ name: "label-near/3",
+
+ text: `x: {
+ label.near: bottom-left
+}
+`,
+ key: `x.label.near`,
+ value: go2.Pointer(`bottom-right`),
+ exp: `x: {
+ label.near: bottom-right
+}
+`,
+ },
+ {
+ name: "label-near/4",
+
+ text: `x: {
+ label: hi {
+ near: bottom-left
+ }
+}
+`,
+ key: `x.label.near`,
+ value: go2.Pointer(`bottom-right`),
+ exp: `x: {
+ label: hi {
+ near: bottom-right
+ }
+}
+`,
+ },
+ {
+ name: "label-near/5",
+
+ text: `x: hi {
+ label: {
+ near: bottom-left
+ }
+}
+`,
+ key: `x.label.near`,
+ value: go2.Pointer(`bottom-right`),
+ exp: `x: hi {
+ label: {
+ near: bottom-right
+ }
+}
+`,
+ },
+ {
+ name: "glob-field/1",
+
+ text: `*.style.fill: red
+a
+b
+`,
+ key: `a.style.fill`,
+ value: go2.Pointer(`blue`),
+ exp: `*.style.fill: red
+a: {style.fill: blue}
+b
+`,
+ },
+ {
+ name: "glob-field/2",
+
+ text: `(* -> *)[*].style.stroke: red
+a -> b
+a -> b
+`,
+ key: `(a -> b)[0].style.stroke`,
+ value: go2.Pointer(`blue`),
+ exp: `(* -> *)[*].style.stroke: red
+a -> b: {style.stroke: blue}
+a -> b
+`,
+ },
+ {
+ name: "glob-field/3",
+
+ text: `(* -> *)[*].style.stroke: red
+a -> b: {style.stroke: blue}
+a -> b
+`,
+ key: `(a -> b)[0].style.stroke`,
+ value: go2.Pointer(`green`),
+ exp: `(* -> *)[*].style.stroke: red
+a -> b: {style.stroke: green}
+a -> b
+`,
+ },
+ {
+ name: "nested-edge-chained/1",
+
+ text: `a: {
+ b: {
+ c
+ }
+}
+
+x -> a.b -> a.b.c
+`,
+ key: `(a.b -> a.b.c)[0].style.stroke`,
+ value: go2.Pointer(`green`),
+ exp: `a: {
+ b: {
+ c
+ }
+}
+
+x -> a.b -> a.b.c
+(a.b -> a.b.c)[0].style.stroke: green
+`,
+ },
+ {
+ name: "nested-edge-chained/2",
+
+ text: `z: {
+ a: {
+ b: {
+ c
+ }
+ }
+ x -> a.b -> a.b.c
+}
+`,
+ key: `(z.a.b -> z.a.b.c)[0].style.stroke`,
+ value: go2.Pointer(`green`),
+ exp: `z: {
+ a: {
+ b: {
+ c
+ }
+ }
+ x -> a.b -> a.b.c
+ (a.b -> a.b.c)[0].style.stroke: green
+}
+`,
+ },
+ {
+ name: "edge-comment",
+
+ text: `x -> y: {
+ # hi
+ style.stroke: blue
+}
+`,
+ key: `(x -> y)[0].style.stroke`,
+ value: go2.Pointer(`green`),
+ exp: `x -> y: {
+ # hi
+ style.stroke: green
+}
+`,
+ },
+ {
+ name: "scenario-child",
+
+ text: `a -> b
+
+scenarios: {
+ x: {
+ hi
+ }
+}
+`,
+ key: `(a -> b)[0].style.stroke-width`,
+ value: go2.Pointer(`3`),
+ boardPath: []string{"x"},
+ exp: `a -> b
+
+scenarios: {
+ x: {
+ hi
+ (a -> b)[0].style.stroke-width: 3
+ }
+}
+`,
+ },
+ {
+ name: "scenario-grandchild",
+
+ text: `a -> b
+
+scenarios: {
+ x: {
+ scenarios: {
+ c: {
+ (a -> b)[0].style.bold: true
+ }
+ }
+ }
+}
+ `,
+ key: `(a -> b)[0].style.stroke-width`,
+ value: go2.Pointer(`3`),
+ boardPath: []string{"x", "c"},
+ exp: `a -> b
+
+scenarios: {
+ x: {
+ scenarios: {
+ c: {
+ (a -> b)[0].style.bold: true
+ (a -> b)[0].style.stroke-width: 3
+ }
+ }
+ }
+}
`,
},
}
@@ -7205,6 +7608,305 @@ scenarios: {
x: null
}
}
+`,
+ },
+ {
+ name: "import/3",
+
+ text: `...@meow
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a -> b
+`,
+ },
+ key: `(a -> b)[0]`,
+ exp: `...@meow
+(a -> b)[0]: null
+`,
+ },
+ {
+ name: "import/4",
+
+ text: `...@meow
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a.link: https://google.com
+`,
+ },
+ key: `a.link`,
+ exp: `...@meow
+a.link: null
+`,
+ },
+ {
+ name: "import/5",
+
+ text: `...@meow
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a -> b: {
+ target-arrowhead: 1
+}
+`,
+ },
+ key: `(a -> b)[0].target-arrowhead`,
+ exp: `...@meow
+(a -> b)[0].target-arrowhead: null
+`,
+ },
+ {
+ name: "import/6",
+
+ text: `...@meow
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a.style.fill: red
+`,
+ },
+ key: `a.style.fill`,
+ exp: `...@meow
+a.style.fill: null
+`,
+ },
+ {
+ name: "import/7",
+
+ text: `...@meow
+a.label.near: center-center
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a
+`,
+ },
+ key: `a.label.near`,
+ exp: `...@meow
+`,
+ },
+ {
+ name: "import/8",
+
+ text: `...@meow
+(a -> b)[0].style.stroke: red
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a -> b
+`,
+ },
+ key: `(a -> b)[0].style.stroke`,
+ exp: `...@meow
+`,
+ },
+ {
+ name: "label-near/1",
+
+ text: `yes: {label.near: center-center}
+`,
+ key: `yes.label.near`,
+ exp: `yes
+`,
+ },
+ {
+ name: "label-near/2",
+
+ text: `yes.label.near: center-center
+`,
+ key: `yes.label.near`,
+ exp: `yes
+`,
+ },
+ {
+ name: "connection-glob",
+
+ text: `* -> *
+a
+b
+`,
+ key: `(a -> b)[0]`,
+ exp: `* -> *
+a
+b
+(a -> b)[0]: null
+`,
+ },
+ {
+ name: "glob-child/1",
+
+ text: `*.b
+a
+`,
+ key: `a.b`,
+ exp: `*.b
+a
+a.b: null
+`,
+ },
+ {
+ name: "delete-imported-layer-obj",
+
+ text: `layers: {
+ x: {
+ ...@meow
+ }
+}
+`,
+ fsTexts: map[string]string{
+ "meow.d2": `a
+`,
+ },
+ boardPath: []string{"x"},
+ key: `a`,
+ exp: `layers: {
+ x: {
+ ...@meow
+ a: null
+ }
+}
+`,
+ },
+ {
+ name: "delete-not-layer-obj",
+
+ text: `b.style.fill: red
+layers: {
+ x: {
+ a
+ }
+}
+`,
+ key: `b.style.fill`,
+ exp: `b
+
+layers: {
+ x: {
+ a
+ }
+}
+`,
+ },
+ {
+ name: "delete-layer-obj",
+
+ text: `layers: {
+ x: {
+ a
+ }
+}
+`,
+ boardPath: []string{"x"},
+ key: `a`,
+ exp: `layers: {
+ x
+}
+`,
+ },
+ {
+ name: "delete-layer-style",
+
+ text: `layers: {
+ x: {
+ a.style.fill: red
+ }
+}
+`,
+ boardPath: []string{"x"},
+ key: `a.style.fill`,
+ exp: `layers: {
+ x: {
+ a
+ }
+}
+`,
+ },
+ {
+ name: "edge-out-layer",
+
+ text: `x: {
+ a -> b
+}
+`,
+ key: `x.(a -> b)[0].style.stroke`,
+ exp: `x: {
+ a -> b
+}
+`,
+ },
+ {
+ name: "edge-in-layer",
+
+ text: `layers: {
+ test: {
+ x: {
+ a -> b
+ }
+ }
+}
+`,
+ boardPath: []string{"test"},
+ key: `x.(a -> b)[0].style.stroke`,
+ exp: `layers: {
+ test: {
+ x: {
+ a -> b
+ }
+ }
+}
+`,
+ },
+ {
+ name: "label-near-in-layer",
+
+ text: `layers: {
+ x: {
+ y: {
+ label.near: center-center
+ }
+ a
+ }
+}
+`,
+ boardPath: []string{"x"},
+ key: `y`,
+ exp: `layers: {
+ x: {
+ a
+ }
+}
+`,
+ },
+ {
+ name: "update-near-in-layer",
+
+ text: `layers: {
+ x: {
+ y: {
+ near: a
+ }
+ a
+ }
+}
+`,
+ boardPath: []string{"x"},
+ key: `y`,
+ exp: `layers: {
+ x: {
+ a
+ }
+}
+`,
+ },
+ {
+ name: "edge-with-glob",
+
+ text: `x -> y
+y
+
+(* -> *)[*].style.opacity: 0.8
+`,
+ key: `(x -> y)[0]`,
+ exp: `x
+y
+
+(* -> *)[*].style.opacity: 0.8
`,
},
}
diff --git a/d2oracle/get.go b/d2oracle/get.go
index 6e5721ee8..3d3daf957 100644
--- a/d2oracle/get.go
+++ b/d2oracle/get.go
@@ -140,8 +140,11 @@ func GetParentID(g *d2graph.Graph, boardPath []string, absID string) (string, er
return obj.Parent.AbsID(), nil
}
-func IsImported(ast *d2ast.Map, obj *d2graph.Object) bool {
+func IsImportedObj(ast *d2ast.Map, obj *d2graph.Object) bool {
for _, ref := range obj.References {
+ if ref.Key.HasGlob() {
+ return true
+ }
if ref.Key.Range.Path != ast.Range.Path {
return true
}
@@ -150,6 +153,22 @@ func IsImported(ast *d2ast.Map, obj *d2graph.Object) bool {
return false
}
+// Glob creations count as imported for now
+// TODO Probably rename later
+func IsImportedEdge(ast *d2ast.Map, edge *d2graph.Edge) bool {
+ for _, ref := range edge.References {
+ // If edge index, the glob is just setting something, not responsible for creating the edge
+ if (ref.Edge.Src.HasGlob() || ref.Edge.Dst.HasGlob()) && ref.MapKey.EdgeIndex == nil {
+ return true
+ }
+ if ref.Edge.Range.Path != ast.Range.Path {
+ return true
+ }
+ }
+
+ return false
+}
+
func GetObj(g *d2graph.Graph, boardPath []string, absID string) *d2graph.Object {
g = GetBoardGraph(g, boardPath)
if g == nil {
@@ -227,3 +246,21 @@ func GetID(key string) string {
return d2format.Format(d2ast.RawString(mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString(), true))
}
+
+func GetWriteableRefs(obj *d2graph.Object, writeableAST *d2ast.Map) (out []d2graph.Reference) {
+ for i, ref := range obj.References {
+ if ref.ScopeAST == writeableAST && ref.Key.Range.Path == writeableAST.Range.Path {
+ out = append(out, obj.References[i])
+ }
+ }
+ return
+}
+
+func GetWriteableEdgeRefs(edge *d2graph.Edge, writeableAST *d2ast.Map) (out []d2graph.EdgeReference) {
+ for i, ref := range edge.References {
+ if ref.ScopeAST == writeableAST {
+ out = append(out, edge.References[i])
+ }
+ }
+ return
+}
diff --git a/d2plugin/exec.go b/d2plugin/exec.go
index 4631ef036..fd3856b11 100644
--- a/d2plugin/exec.go
+++ b/d2plugin/exec.go
@@ -201,3 +201,59 @@ func (p *execPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error)
return stdout, nil
}
+
+func (p *execPlugin) RouteEdges(ctx context.Context, g *d2graph.Graph, edges []*d2graph.Edge) error {
+ ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
+ defer cancel()
+
+ graphBytes, err := d2graph.SerializeGraph(g)
+ if err != nil {
+ return err
+ }
+
+ var g2 d2graph.Graph
+ err = d2graph.DeserializeGraph(graphBytes, &g2)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal json: %w", err)
+ }
+ g2.Edges = edges
+ graphBytes2, err := d2graph.SerializeGraph(&g2)
+ if err != nil {
+ return err
+ }
+
+ in := routeEdgesInput{
+ G: graphBytes,
+ GEdges: graphBytes2,
+ }
+
+ b, err := json.Marshal(in)
+ if err != nil {
+ return err
+ }
+
+ args := []string{"routeedges"}
+ for k, v := range p.opts {
+ args = append(args, fmt.Sprintf("--%s", k), v)
+ }
+ cmd := exec.CommandContext(ctx, p.path, args...)
+
+ buffer := bytes.Buffer{}
+ buffer.Write(b)
+ cmd.Stdin = &buffer
+
+ stdout, err := cmd.Output()
+ if err != nil {
+ ee := &exec.ExitError{}
+ if errors.As(err, &ee) && len(ee.Stderr) > 0 {
+ return fmt.Errorf("%v\nstderr:\n%s", ee, ee.Stderr)
+ }
+ return err
+ }
+ err = d2graph.DeserializeGraph(stdout, g)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal json: %w", err)
+ }
+
+ return nil
+}
diff --git a/d2plugin/plugin.go b/d2plugin/plugin.go
index f12868faf..6708e5e03 100644
--- a/d2plugin/plugin.go
+++ b/d2plugin/plugin.go
@@ -85,6 +85,11 @@ type RoutingPlugin interface {
RouteEdges(context.Context, *d2graph.Graph, []*d2graph.Edge) error
}
+type routeEdgesInput struct {
+ G []byte `json:"g"`
+ GEdges []byte `json:"gEdges"`
+}
+
// PluginInfo is the current info information of a plugin.
// note: The two fields Type and Path are not set by the plugin
// itself but only in ListPlugins.
diff --git a/d2plugin/serve.go b/d2plugin/serve.go
index 9cae5ca2f..c975217ce 100644
--- a/d2plugin/serve.go
+++ b/d2plugin/serve.go
@@ -58,6 +58,12 @@ func Serve(p Plugin) xmain.RunFunc {
return layout(ctx, p, ms)
case "postprocess":
return postProcess(ctx, p, ms)
+ case "routeedges":
+ routingPlugin, ok := p.(RoutingPlugin)
+ if !ok {
+ return fmt.Errorf("plugin has routing feature but does not implement RoutingPlugin")
+ }
+ return routeEdges(ctx, routingPlugin, ms)
default:
return xmain.UsageErrorf("unrecognized command: %s", subcmd)
}
@@ -137,3 +143,41 @@ func postProcess(ctx context.Context, p Plugin, ms *xmain.State) error {
}
return nil
}
+
+func routeEdges(ctx context.Context, p RoutingPlugin, ms *xmain.State) error {
+ inRaw, err := io.ReadAll(ms.Stdin)
+ if err != nil {
+ return err
+ }
+
+ var in routeEdgesInput
+ err = json.Unmarshal(inRaw, &in)
+ if err != nil {
+ return err
+ }
+
+ var g d2graph.Graph
+ if err := d2graph.DeserializeGraph(in.G, &g); err != nil {
+ return fmt.Errorf("failed to unmarshal input graph to graph: %s", in)
+ }
+
+ var gedges d2graph.Graph
+ if err := d2graph.DeserializeGraph(in.GEdges, &gedges); err != nil {
+ return fmt.Errorf("failed to unmarshal input edges graph to graph: %s", in)
+ }
+
+ err = p.RouteEdges(ctx, &g, gedges.Edges)
+ if err != nil {
+ return err
+ }
+
+ b, err := d2graph.SerializeGraph(&g)
+ if err != nil {
+ return err
+ }
+ _, err = ms.Stdout.Write(b)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go
index 38918c88b..edb8f547c 100644
--- a/d2renderers/d2sketch/sketch.go
+++ b/d2renderers/d2sketch/sketch.go
@@ -321,29 +321,70 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
}
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
- roughness := 0.5
- js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
- paths, err := computeRoughPathData(r, js)
- if err != nil {
- return "", err
- }
- output := ""
animatedClass := ""
if connection.Animated {
animatedClass = " animated-connection"
}
- pathEl := d2themes.NewThemableElement("path")
- pathEl.Fill = color.None
- pathEl.Stroke = connection.Stroke
- pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
- pathEl.Style = connection.CSSStyle()
- pathEl.Attributes = attrs
- for _, p := range paths {
- pathEl.D = p
- output += pathEl.Render()
+ if connection.Animated {
+ // If connection is animated and bidirectional
+ if (connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead) {
+ // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
+ path1, path2, err := svg.SplitPath(path, 0.5)
+
+ if err != nil {
+ return "", err
+ }
+
+ pathEl1 := d2themes.NewThemableElement("path")
+ pathEl1.D = path1
+ pathEl1.Fill = color.None
+ pathEl1.Stroke = connection.Stroke
+ pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl1.Style = connection.CSSStyle()
+ pathEl1.Style += "animation-direction: reverse;"
+ pathEl1.Attributes = attrs
+
+ pathEl2 := d2themes.NewThemableElement("path")
+ pathEl2.D = path2
+ pathEl2.Fill = color.None
+ pathEl2.Stroke = connection.Stroke
+ pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl2.Style = connection.CSSStyle()
+ pathEl2.Attributes = attrs
+ return pathEl1.Render() + " " + pathEl2.Render(), nil
+ } else {
+ pathEl := d2themes.NewThemableElement("path")
+ pathEl.D = path
+ pathEl.Fill = color.None
+ pathEl.Stroke = connection.Stroke
+ pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl.Style = connection.CSSStyle()
+ pathEl.Attributes = attrs
+ return pathEl.Render(), nil
+ }
+ } else {
+ roughness := 0.5
+ js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
+ paths, err := computeRoughPathData(r, js)
+ if err != nil {
+ return "", err
+ }
+
+ output := ""
+
+ pathEl := d2themes.NewThemableElement("path")
+ pathEl.Fill = color.None
+ pathEl.Stroke = connection.Stroke
+ pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl.Style = connection.CSSStyle()
+ pathEl.Attributes = attrs
+ for _, p := range paths {
+ pathEl.D = p
+ output += pathEl.Render()
+ }
+ return output, nil
}
- return output, nil
}
// TODO cleanup
@@ -802,6 +843,13 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
stroke,
BG_COLOR,
)
+ case d2target.CircleArrowhead:
+ arrowJS = fmt.Sprintf(
+ `node = rc.circle(-2, -1, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`,
+ strokeWidth,
+ stroke,
+ BG_COLOR,
+ )
}
return
}
diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
index 84ace1a20..c847c50bd 100644
--- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
@@ -1,4 +1,4 @@
-
diff --git a/docs/assets/syntax.png b/docs/assets/syntax.png
deleted file mode 100644
index a836246ed..000000000
Binary files a/docs/assets/syntax.png and /dev/null differ
diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go
index a47f2f819..5a2f925d7 100644
--- a/e2etests-cli/main_test.go
+++ b/e2etests-cli/main_test.go
@@ -765,6 +765,18 @@ i used to read
assert.Testdata(t, ".svg", svg)
},
},
+ {
+ name: "theme-pdf",
+ skipCI: true,
+ run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
+ writeFile(t, dir, "in.d2", `x -> y`)
+ err := runTestMain(t, ctx, dir, env, "--theme=5", "in.d2", "out.pdf")
+ assert.Success(t, err)
+
+ pdf := readFile(t, dir, "out.pdf")
+ testdataIgnoreDiff(t, ".pdf", pdf)
+ },
+ },
{
name: "renamed-board",
skipCI: true,
diff --git a/e2etests-cli/testdata/TestCLI_E2E/abspath.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/abspath.exp.svg
index 4c6fec44b..e68e7efce 100644
--- a/e2etests-cli/testdata/TestCLI_E2E/abspath.exp.svg
+++ b/e2etests-cli/testdata/TestCLI_E2E/abspath.exp.svg
@@ -1,4 +1,4 @@
-โฌคCheck PINSearch NetworkReadyOffโฌคEnter PINโฌค /check PIN[pin invalid][pin OK][pin OK]network foundpower offpower offpower off
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ .d2-2887586175 .fill-N1{fill:#0A0F25;}
+ .d2-2887586175 .fill-N2{fill:#676C7E;}
+ .d2-2887586175 .fill-N3{fill:#9499AB;}
+ .d2-2887586175 .fill-N4{fill:#CFD2DD;}
+ .d2-2887586175 .fill-N5{fill:#DEE1EB;}
+ .d2-2887586175 .fill-N6{fill:#EEF1F8;}
+ .d2-2887586175 .fill-N7{fill:#FFFFFF;}
+ .d2-2887586175 .fill-B1{fill:#0D32B2;}
+ .d2-2887586175 .fill-B2{fill:#0D32B2;}
+ .d2-2887586175 .fill-B3{fill:#E3E9FD;}
+ .d2-2887586175 .fill-B4{fill:#E3E9FD;}
+ .d2-2887586175 .fill-B5{fill:#EDF0FD;}
+ .d2-2887586175 .fill-B6{fill:#F7F8FE;}
+ .d2-2887586175 .fill-AA2{fill:#4A6FF3;}
+ .d2-2887586175 .fill-AA4{fill:#EDF0FD;}
+ .d2-2887586175 .fill-AA5{fill:#F7F8FE;}
+ .d2-2887586175 .fill-AB4{fill:#EDF0FD;}
+ .d2-2887586175 .fill-AB5{fill:#F7F8FE;}
+ .d2-2887586175 .stroke-N1{stroke:#0A0F25;}
+ .d2-2887586175 .stroke-N2{stroke:#676C7E;}
+ .d2-2887586175 .stroke-N3{stroke:#9499AB;}
+ .d2-2887586175 .stroke-N4{stroke:#CFD2DD;}
+ .d2-2887586175 .stroke-N5{stroke:#DEE1EB;}
+ .d2-2887586175 .stroke-N6{stroke:#EEF1F8;}
+ .d2-2887586175 .stroke-N7{stroke:#FFFFFF;}
+ .d2-2887586175 .stroke-B1{stroke:#0D32B2;}
+ .d2-2887586175 .stroke-B2{stroke:#0D32B2;}
+ .d2-2887586175 .stroke-B3{stroke:#E3E9FD;}
+ .d2-2887586175 .stroke-B4{stroke:#E3E9FD;}
+ .d2-2887586175 .stroke-B5{stroke:#EDF0FD;}
+ .d2-2887586175 .stroke-B6{stroke:#F7F8FE;}
+ .d2-2887586175 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-2887586175 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-2887586175 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-2887586175 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-2887586175 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-2887586175 .background-color-N1{background-color:#0A0F25;}
+ .d2-2887586175 .background-color-N2{background-color:#676C7E;}
+ .d2-2887586175 .background-color-N3{background-color:#9499AB;}
+ .d2-2887586175 .background-color-N4{background-color:#CFD2DD;}
+ .d2-2887586175 .background-color-N5{background-color:#DEE1EB;}
+ .d2-2887586175 .background-color-N6{background-color:#EEF1F8;}
+ .d2-2887586175 .background-color-N7{background-color:#FFFFFF;}
+ .d2-2887586175 .background-color-B1{background-color:#0D32B2;}
+ .d2-2887586175 .background-color-B2{background-color:#0D32B2;}
+ .d2-2887586175 .background-color-B3{background-color:#E3E9FD;}
+ .d2-2887586175 .background-color-B4{background-color:#E3E9FD;}
+ .d2-2887586175 .background-color-B5{background-color:#EDF0FD;}
+ .d2-2887586175 .background-color-B6{background-color:#F7F8FE;}
+ .d2-2887586175 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-2887586175 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-2887586175 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-2887586175 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-2887586175 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-2887586175 .color-N1{color:#0A0F25;}
+ .d2-2887586175 .color-N2{color:#676C7E;}
+ .d2-2887586175 .color-N3{color:#9499AB;}
+ .d2-2887586175 .color-N4{color:#CFD2DD;}
+ .d2-2887586175 .color-N5{color:#DEE1EB;}
+ .d2-2887586175 .color-N6{color:#EEF1F8;}
+ .d2-2887586175 .color-N7{color:#FFFFFF;}
+ .d2-2887586175 .color-B1{color:#0D32B2;}
+ .d2-2887586175 .color-B2{color:#0D32B2;}
+ .d2-2887586175 .color-B3{color:#E3E9FD;}
+ .d2-2887586175 .color-B4{color:#E3E9FD;}
+ .d2-2887586175 .color-B5{color:#EDF0FD;}
+ .d2-2887586175 .color-B6{color:#F7F8FE;}
+ .d2-2887586175 .color-AA2{color:#4A6FF3;}
+ .d2-2887586175 .color-AA4{color:#EDF0FD;}
+ .d2-2887586175 .color-AA5{color:#F7F8FE;}
+ .d2-2887586175 .color-AB4{color:#EDF0FD;}
+ .d2-2887586175 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>โฌคCheck PINSearch NetworkReadyOffโฌคEnter PINโฌค /check PIN[pin invalid][pin OK][pin OK]network foundpower offpower offpower off
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/glob_dimensions/elk/board.exp.json b/e2etests/testdata/regression/glob_dimensions/elk/board.exp.json
index aec42cecc..d6f22abdc 100644
--- a/e2etests/testdata/regression/glob_dimensions/elk/board.exp.json
+++ b/e2etests/testdata/regression/glob_dimensions/elk/board.exp.json
@@ -7,11 +7,11 @@
"id": "start",
"type": "oval",
"pos": {
- "x": 153,
+ "x": 163,
"y": 12
},
- "width": 30,
- "height": 30,
+ "width": 10,
+ "height": 10,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -49,10 +49,10 @@
"type": "rectangle",
"pos": {
"x": 37,
- "y": 112
+ "y": 103
},
"width": 262,
- "height": 658,
+ "height": 655,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -89,11 +89,11 @@
"id": "Check PIN.start",
"type": "oval",
"pos": {
- "x": 151,
- "y": 162
+ "x": 161,
+ "y": 153
},
- "width": 30,
- "height": 30,
+ "width": 10,
+ "height": 10,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -131,7 +131,7 @@
"type": "rectangle",
"pos": {
"x": 111,
- "y": 262
+ "y": 244
},
"width": 111,
"height": 66,
@@ -172,7 +172,7 @@
"type": "diamond",
"pos": {
"x": 156,
- "y": 509
+ "y": 491
},
"width": 20,
"height": 20,
@@ -211,11 +211,11 @@
"id": "Check PIN.end",
"type": "oval",
"pos": {
- "x": 151,
- "y": 690
+ "x": 161,
+ "y": 672
},
- "width": 30,
- "height": 30,
+ "width": 10,
+ "height": 10,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -244,7 +244,7 @@
"underline": false,
"labelWidth": 9,
"labelHeight": 21,
- "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
"zIndex": 0,
"level": 2
},
@@ -253,7 +253,7 @@
"type": "rectangle",
"pos": {
"x": 29,
- "y": 941
+ "y": 929
},
"width": 159,
"height": 66,
@@ -294,7 +294,7 @@
"type": "rectangle",
"pos": {
"x": 15,
- "y": 1178
+ "y": 1166
},
"width": 89,
"height": 66,
@@ -335,7 +335,7 @@
"type": "rectangle",
"pos": {
"x": 95,
- "y": 1415
+ "y": 1403
},
"width": 120,
"height": 66,
@@ -399,11 +399,11 @@
"route": [
{
"x": 169,
- "y": 42
+ "y": 22
},
{
"x": 168,
- "y": 112
+ "y": 103
}
],
"animated": false,
@@ -437,11 +437,11 @@
"route": [
{
"x": 167,
- "y": 192
+ "y": 163
},
{
"x": 166,
- "y": 262
+ "y": 244
}
],
"animated": false,
@@ -475,19 +475,19 @@
"route": [
{
"x": 123.5,
- "y": 328
+ "y": 310
},
{
"x": 123.5,
- "y": 469
+ "y": 451
},
{
"x": 163.16600036621094,
- "y": 469
+ "y": 451
},
{
"x": 163,
- "y": 512
+ "y": 494
}
],
"animated": false,
@@ -521,19 +521,19 @@
"route": [
{
"x": 170,
- "y": 513
+ "y": 495
},
{
"x": 169.83299255371094,
- "y": 469
+ "y": 451
},
{
"x": 209.5,
- "y": 469
+ "y": 451
},
{
"x": 209.5,
- "y": 328
+ "y": 310
}
],
"animated": false,
@@ -567,11 +567,11 @@
"route": [
{
"x": 166,
- "y": 528
+ "y": 510
},
{
"x": 167,
- "y": 690
+ "y": 672
}
],
"animated": false,
@@ -605,11 +605,11 @@
"route": [
{
"x": 108.75,
- "y": 770
+ "y": 758
},
{
"x": 108.75,
- "y": 941
+ "y": 929
}
],
"animated": false,
@@ -643,11 +643,11 @@
"route": [
{
"x": 60,
- "y": 1007
+ "y": 995
},
{
"x": 60,
- "y": 1178
+ "y": 1166
}
],
"animated": false,
@@ -681,19 +681,19 @@
"route": [
{
"x": 240.99899291992188,
- "y": 770
+ "y": 758
},
{
"x": 240.99899291992188,
- "y": 1375
+ "y": 1363
},
{
"x": 185.25,
- "y": 1375
+ "y": 1363
},
{
"x": 185.25,
- "y": 1415
+ "y": 1403
}
],
"animated": false,
@@ -727,11 +727,11 @@
"route": [
{
"x": 157.5,
- "y": 1007
+ "y": 995
},
{
"x": 157.5,
- "y": 1415
+ "y": 1403
}
],
"animated": false,
@@ -765,19 +765,19 @@
"route": [
{
"x": 60,
- "y": 1244
+ "y": 1232
},
{
"x": 60,
- "y": 1375
+ "y": 1363
},
{
"x": 125.25,
- "y": 1375
+ "y": 1363
},
{
"x": 125.25,
- "y": 1415
+ "y": 1403
}
],
"animated": false,
diff --git a/e2etests/testdata/regression/glob_dimensions/elk/sketch.exp.svg b/e2etests/testdata/regression/glob_dimensions/elk/sketch.exp.svg
index d834ef015..0079d186d 100644
--- a/e2etests/testdata/regression/glob_dimensions/elk/sketch.exp.svg
+++ b/e2etests/testdata/regression/glob_dimensions/elk/sketch.exp.svg
@@ -1,23 +1,23 @@
-โฌคCheck PINSearch NetworkReadyOffโฌคEnter PINโฌค /check PIN[pin invalid][pin OK][pin OK]network foundpower offpower offpower off
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ .d2-1524816052 .fill-N1{fill:#0A0F25;}
+ .d2-1524816052 .fill-N2{fill:#676C7E;}
+ .d2-1524816052 .fill-N3{fill:#9499AB;}
+ .d2-1524816052 .fill-N4{fill:#CFD2DD;}
+ .d2-1524816052 .fill-N5{fill:#DEE1EB;}
+ .d2-1524816052 .fill-N6{fill:#EEF1F8;}
+ .d2-1524816052 .fill-N7{fill:#FFFFFF;}
+ .d2-1524816052 .fill-B1{fill:#0D32B2;}
+ .d2-1524816052 .fill-B2{fill:#0D32B2;}
+ .d2-1524816052 .fill-B3{fill:#E3E9FD;}
+ .d2-1524816052 .fill-B4{fill:#E3E9FD;}
+ .d2-1524816052 .fill-B5{fill:#EDF0FD;}
+ .d2-1524816052 .fill-B6{fill:#F7F8FE;}
+ .d2-1524816052 .fill-AA2{fill:#4A6FF3;}
+ .d2-1524816052 .fill-AA4{fill:#EDF0FD;}
+ .d2-1524816052 .fill-AA5{fill:#F7F8FE;}
+ .d2-1524816052 .fill-AB4{fill:#EDF0FD;}
+ .d2-1524816052 .fill-AB5{fill:#F7F8FE;}
+ .d2-1524816052 .stroke-N1{stroke:#0A0F25;}
+ .d2-1524816052 .stroke-N2{stroke:#676C7E;}
+ .d2-1524816052 .stroke-N3{stroke:#9499AB;}
+ .d2-1524816052 .stroke-N4{stroke:#CFD2DD;}
+ .d2-1524816052 .stroke-N5{stroke:#DEE1EB;}
+ .d2-1524816052 .stroke-N6{stroke:#EEF1F8;}
+ .d2-1524816052 .stroke-N7{stroke:#FFFFFF;}
+ .d2-1524816052 .stroke-B1{stroke:#0D32B2;}
+ .d2-1524816052 .stroke-B2{stroke:#0D32B2;}
+ .d2-1524816052 .stroke-B3{stroke:#E3E9FD;}
+ .d2-1524816052 .stroke-B4{stroke:#E3E9FD;}
+ .d2-1524816052 .stroke-B5{stroke:#EDF0FD;}
+ .d2-1524816052 .stroke-B6{stroke:#F7F8FE;}
+ .d2-1524816052 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-1524816052 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-1524816052 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-1524816052 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-1524816052 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-1524816052 .background-color-N1{background-color:#0A0F25;}
+ .d2-1524816052 .background-color-N2{background-color:#676C7E;}
+ .d2-1524816052 .background-color-N3{background-color:#9499AB;}
+ .d2-1524816052 .background-color-N4{background-color:#CFD2DD;}
+ .d2-1524816052 .background-color-N5{background-color:#DEE1EB;}
+ .d2-1524816052 .background-color-N6{background-color:#EEF1F8;}
+ .d2-1524816052 .background-color-N7{background-color:#FFFFFF;}
+ .d2-1524816052 .background-color-B1{background-color:#0D32B2;}
+ .d2-1524816052 .background-color-B2{background-color:#0D32B2;}
+ .d2-1524816052 .background-color-B3{background-color:#E3E9FD;}
+ .d2-1524816052 .background-color-B4{background-color:#E3E9FD;}
+ .d2-1524816052 .background-color-B5{background-color:#EDF0FD;}
+ .d2-1524816052 .background-color-B6{background-color:#F7F8FE;}
+ .d2-1524816052 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-1524816052 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-1524816052 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-1524816052 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-1524816052 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-1524816052 .color-N1{color:#0A0F25;}
+ .d2-1524816052 .color-N2{color:#676C7E;}
+ .d2-1524816052 .color-N3{color:#9499AB;}
+ .d2-1524816052 .color-N4{color:#CFD2DD;}
+ .d2-1524816052 .color-N5{color:#DEE1EB;}
+ .d2-1524816052 .color-N6{color:#EEF1F8;}
+ .d2-1524816052 .color-N7{color:#FFFFFF;}
+ .d2-1524816052 .color-B1{color:#0D32B2;}
+ .d2-1524816052 .color-B2{color:#0D32B2;}
+ .d2-1524816052 .color-B3{color:#E3E9FD;}
+ .d2-1524816052 .color-B4{color:#E3E9FD;}
+ .d2-1524816052 .color-B5{color:#EDF0FD;}
+ .d2-1524816052 .color-B6{color:#F7F8FE;}
+ .d2-1524816052 .color-AA2{color:#4A6FF3;}
+ .d2-1524816052 .color-AA4{color:#EDF0FD;}
+ .d2-1524816052 .color-AA5{color:#F7F8FE;}
+ .d2-1524816052 .color-AB4{color:#EDF0FD;}
+ .d2-1524816052 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>โฌคCheck PINSearch NetworkReadyOffโฌคEnter PINโฌค /check PIN[pin invalid][pin OK][pin OK]network foundpower offpower offpower off
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/grid_image_label_position/dagre/sketch.exp.svg b/e2etests/testdata/regression/grid_image_label_position/dagre/sketch.exp.svg
index de3bf9c23..680e3b47e 100644
--- a/e2etests/testdata/regression/grid_image_label_position/dagre/sketch.exp.svg
+++ b/e2etests/testdata/regression/grid_image_label_position/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-teamwork: having someone to blame
-
-
+ .d2-3251595454 .fill-N1{fill:#0A0F25;}
+ .d2-3251595454 .fill-N2{fill:#676C7E;}
+ .d2-3251595454 .fill-N3{fill:#9499AB;}
+ .d2-3251595454 .fill-N4{fill:#CFD2DD;}
+ .d2-3251595454 .fill-N5{fill:#DEE1EB;}
+ .d2-3251595454 .fill-N6{fill:#EEF1F8;}
+ .d2-3251595454 .fill-N7{fill:#FFFFFF;}
+ .d2-3251595454 .fill-B1{fill:#0D32B2;}
+ .d2-3251595454 .fill-B2{fill:#0D32B2;}
+ .d2-3251595454 .fill-B3{fill:#E3E9FD;}
+ .d2-3251595454 .fill-B4{fill:#E3E9FD;}
+ .d2-3251595454 .fill-B5{fill:#EDF0FD;}
+ .d2-3251595454 .fill-B6{fill:#F7F8FE;}
+ .d2-3251595454 .fill-AA2{fill:#4A6FF3;}
+ .d2-3251595454 .fill-AA4{fill:#EDF0FD;}
+ .d2-3251595454 .fill-AA5{fill:#F7F8FE;}
+ .d2-3251595454 .fill-AB4{fill:#EDF0FD;}
+ .d2-3251595454 .fill-AB5{fill:#F7F8FE;}
+ .d2-3251595454 .stroke-N1{stroke:#0A0F25;}
+ .d2-3251595454 .stroke-N2{stroke:#676C7E;}
+ .d2-3251595454 .stroke-N3{stroke:#9499AB;}
+ .d2-3251595454 .stroke-N4{stroke:#CFD2DD;}
+ .d2-3251595454 .stroke-N5{stroke:#DEE1EB;}
+ .d2-3251595454 .stroke-N6{stroke:#EEF1F8;}
+ .d2-3251595454 .stroke-N7{stroke:#FFFFFF;}
+ .d2-3251595454 .stroke-B1{stroke:#0D32B2;}
+ .d2-3251595454 .stroke-B2{stroke:#0D32B2;}
+ .d2-3251595454 .stroke-B3{stroke:#E3E9FD;}
+ .d2-3251595454 .stroke-B4{stroke:#E3E9FD;}
+ .d2-3251595454 .stroke-B5{stroke:#EDF0FD;}
+ .d2-3251595454 .stroke-B6{stroke:#F7F8FE;}
+ .d2-3251595454 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-3251595454 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-3251595454 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-3251595454 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-3251595454 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-3251595454 .background-color-N1{background-color:#0A0F25;}
+ .d2-3251595454 .background-color-N2{background-color:#676C7E;}
+ .d2-3251595454 .background-color-N3{background-color:#9499AB;}
+ .d2-3251595454 .background-color-N4{background-color:#CFD2DD;}
+ .d2-3251595454 .background-color-N5{background-color:#DEE1EB;}
+ .d2-3251595454 .background-color-N6{background-color:#EEF1F8;}
+ .d2-3251595454 .background-color-N7{background-color:#FFFFFF;}
+ .d2-3251595454 .background-color-B1{background-color:#0D32B2;}
+ .d2-3251595454 .background-color-B2{background-color:#0D32B2;}
+ .d2-3251595454 .background-color-B3{background-color:#E3E9FD;}
+ .d2-3251595454 .background-color-B4{background-color:#E3E9FD;}
+ .d2-3251595454 .background-color-B5{background-color:#EDF0FD;}
+ .d2-3251595454 .background-color-B6{background-color:#F7F8FE;}
+ .d2-3251595454 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-3251595454 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-3251595454 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-3251595454 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-3251595454 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-3251595454 .color-N1{color:#0A0F25;}
+ .d2-3251595454 .color-N2{color:#676C7E;}
+ .d2-3251595454 .color-N3{color:#9499AB;}
+ .d2-3251595454 .color-N4{color:#CFD2DD;}
+ .d2-3251595454 .color-N5{color:#DEE1EB;}
+ .d2-3251595454 .color-N6{color:#EEF1F8;}
+ .d2-3251595454 .color-N7{color:#FFFFFF;}
+ .d2-3251595454 .color-B1{color:#0D32B2;}
+ .d2-3251595454 .color-B2{color:#0D32B2;}
+ .d2-3251595454 .color-B3{color:#E3E9FD;}
+ .d2-3251595454 .color-B4{color:#E3E9FD;}
+ .d2-3251595454 .color-B5{color:#EDF0FD;}
+ .d2-3251595454 .color-B6{color:#F7F8FE;}
+ .d2-3251595454 .color-AA2{color:#4A6FF3;}
+ .d2-3251595454 .color-AA4{color:#EDF0FD;}
+ .d2-3251595454 .color-AA5{color:#F7F8FE;}
+ .d2-3251595454 .color-AB4{color:#EDF0FD;}
+ .d2-3251595454 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>teamwork: having someone to blame
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/just-width/elk/board.exp.json b/e2etests/testdata/regression/just-width/elk/board.exp.json
index f88d632ff..90db64280 100644
--- a/e2etests/testdata/regression/just-width/elk/board.exp.json
+++ b/e2etests/testdata/regression/just-width/elk/board.exp.json
@@ -10,7 +10,7 @@
"x": 12,
"y": 12
},
- "width": 262,
+ "width": 100,
"height": 61,
"opacity": 1,
"strokeDash": 0,
@@ -40,7 +40,7 @@
"underline": false,
"labelWidth": 262,
"labelHeight": 21,
- "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
"zIndex": 0,
"level": 1
}
diff --git a/e2etests/testdata/regression/just-width/elk/sketch.exp.svg b/e2etests/testdata/regression/just-width/elk/sketch.exp.svg
index 4125f4e3f..83168ccf1 100644
--- a/e2etests/testdata/regression/just-width/elk/sketch.exp.svg
+++ b/e2etests/testdata/regression/just-width/elk/sketch.exp.svg
@@ -1,9 +1,9 @@
-teamwork: having someone to blame
-
-
+ .d2-1014957270 .fill-N1{fill:#0A0F25;}
+ .d2-1014957270 .fill-N2{fill:#676C7E;}
+ .d2-1014957270 .fill-N3{fill:#9499AB;}
+ .d2-1014957270 .fill-N4{fill:#CFD2DD;}
+ .d2-1014957270 .fill-N5{fill:#DEE1EB;}
+ .d2-1014957270 .fill-N6{fill:#EEF1F8;}
+ .d2-1014957270 .fill-N7{fill:#FFFFFF;}
+ .d2-1014957270 .fill-B1{fill:#0D32B2;}
+ .d2-1014957270 .fill-B2{fill:#0D32B2;}
+ .d2-1014957270 .fill-B3{fill:#E3E9FD;}
+ .d2-1014957270 .fill-B4{fill:#E3E9FD;}
+ .d2-1014957270 .fill-B5{fill:#EDF0FD;}
+ .d2-1014957270 .fill-B6{fill:#F7F8FE;}
+ .d2-1014957270 .fill-AA2{fill:#4A6FF3;}
+ .d2-1014957270 .fill-AA4{fill:#EDF0FD;}
+ .d2-1014957270 .fill-AA5{fill:#F7F8FE;}
+ .d2-1014957270 .fill-AB4{fill:#EDF0FD;}
+ .d2-1014957270 .fill-AB5{fill:#F7F8FE;}
+ .d2-1014957270 .stroke-N1{stroke:#0A0F25;}
+ .d2-1014957270 .stroke-N2{stroke:#676C7E;}
+ .d2-1014957270 .stroke-N3{stroke:#9499AB;}
+ .d2-1014957270 .stroke-N4{stroke:#CFD2DD;}
+ .d2-1014957270 .stroke-N5{stroke:#DEE1EB;}
+ .d2-1014957270 .stroke-N6{stroke:#EEF1F8;}
+ .d2-1014957270 .stroke-N7{stroke:#FFFFFF;}
+ .d2-1014957270 .stroke-B1{stroke:#0D32B2;}
+ .d2-1014957270 .stroke-B2{stroke:#0D32B2;}
+ .d2-1014957270 .stroke-B3{stroke:#E3E9FD;}
+ .d2-1014957270 .stroke-B4{stroke:#E3E9FD;}
+ .d2-1014957270 .stroke-B5{stroke:#EDF0FD;}
+ .d2-1014957270 .stroke-B6{stroke:#F7F8FE;}
+ .d2-1014957270 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-1014957270 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-1014957270 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-1014957270 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-1014957270 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-1014957270 .background-color-N1{background-color:#0A0F25;}
+ .d2-1014957270 .background-color-N2{background-color:#676C7E;}
+ .d2-1014957270 .background-color-N3{background-color:#9499AB;}
+ .d2-1014957270 .background-color-N4{background-color:#CFD2DD;}
+ .d2-1014957270 .background-color-N5{background-color:#DEE1EB;}
+ .d2-1014957270 .background-color-N6{background-color:#EEF1F8;}
+ .d2-1014957270 .background-color-N7{background-color:#FFFFFF;}
+ .d2-1014957270 .background-color-B1{background-color:#0D32B2;}
+ .d2-1014957270 .background-color-B2{background-color:#0D32B2;}
+ .d2-1014957270 .background-color-B3{background-color:#E3E9FD;}
+ .d2-1014957270 .background-color-B4{background-color:#E3E9FD;}
+ .d2-1014957270 .background-color-B5{background-color:#EDF0FD;}
+ .d2-1014957270 .background-color-B6{background-color:#F7F8FE;}
+ .d2-1014957270 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-1014957270 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-1014957270 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-1014957270 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-1014957270 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-1014957270 .color-N1{color:#0A0F25;}
+ .d2-1014957270 .color-N2{color:#676C7E;}
+ .d2-1014957270 .color-N3{color:#9499AB;}
+ .d2-1014957270 .color-N4{color:#CFD2DD;}
+ .d2-1014957270 .color-N5{color:#DEE1EB;}
+ .d2-1014957270 .color-N6{color:#EEF1F8;}
+ .d2-1014957270 .color-N7{color:#FFFFFF;}
+ .d2-1014957270 .color-B1{color:#0D32B2;}
+ .d2-1014957270 .color-B2{color:#0D32B2;}
+ .d2-1014957270 .color-B3{color:#E3E9FD;}
+ .d2-1014957270 .color-B4{color:#E3E9FD;}
+ .d2-1014957270 .color-B5{color:#EDF0FD;}
+ .d2-1014957270 .color-B6{color:#F7F8FE;}
+ .d2-1014957270 .color-AA2{color:#4A6FF3;}
+ .d2-1014957270 .color-AA4{color:#EDF0FD;}
+ .d2-1014957270 .color-AA5{color:#F7F8FE;}
+ .d2-1014957270 .color-AB4{color:#EDF0FD;}
+ .d2-1014957270 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>teamwork: having someone to blame
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/link_with_ampersand/dagre/sketch.exp.svg b/e2etests/testdata/regression/link_with_ampersand/dagre/sketch.exp.svg
index 6d03131aa..9acfedb40 100644
--- a/e2etests/testdata/regression/link_with_ampersand/dagre/sketch.exp.svg
+++ b/e2etests/testdata/regression/link_with_ampersand/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-3267239171 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
diff --git a/e2etests/testdata/stable/animated/elk/sketch.exp.svg b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
index fa3a93bef..bffee0710 100644
--- a/e2etests/testdata/stable/animated/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
@@ -1,4 +1,4 @@
-your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-838869033 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
diff --git a/e2etests/testdata/stable/array-classes/dagre/sketch.exp.svg b/e2etests/testdata/stable/array-classes/dagre/sketch.exp.svg
index f6d96c2bf..f4633a3d0 100644
--- a/e2etests/testdata/stable/array-classes/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/array-classes/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-containera header
+
containera header
a line of text and an
{
indented: "block",
@@ -845,7 +845,7 @@
}
walk into a bar.
-
+
diff --git a/e2etests/testdata/stable/markdown_stroke_fill/elk/board.exp.json b/e2etests/testdata/stable/markdown_stroke_fill/elk/board.exp.json
index 152457511..3a3f429c3 100644
--- a/e2etests/testdata/stable/markdown_stroke_fill/elk/board.exp.json
+++ b/e2etests/testdata/stable/markdown_stroke_fill/elk/board.exp.json
@@ -58,7 +58,7 @@
"strokeWidth": 2,
"borderRadius": 0,
"fill": "transparent",
- "stroke": "darkorange",
+ "stroke": "N1",
"shadow": false,
"3d": false,
"multiple": false,
@@ -75,7 +75,7 @@
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
- "color": "N1",
+ "color": "darkorange",
"italic": false,
"bold": false,
"underline": false,
@@ -98,7 +98,7 @@
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#CEEDEE",
- "stroke": "red",
+ "stroke": "N1",
"shadow": false,
"3d": false,
"multiple": false,
@@ -115,7 +115,7 @@
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
- "color": "N1",
+ "color": "red",
"italic": false,
"bold": false,
"underline": false,
diff --git a/e2etests/testdata/stable/markdown_stroke_fill/elk/sketch.exp.svg b/e2etests/testdata/stable/markdown_stroke_fill/elk/sketch.exp.svg
index 24bea82fe..6a01ad4c5 100644
--- a/e2etests/testdata/stable/markdown_stroke_fill/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/markdown_stroke_fill/elk/sketch.exp.svg
@@ -1,20 +1,20 @@
-containera header
+
containera header
a line of text and an
{
indented: "block",
@@ -845,7 +845,7 @@
}
walk into a bar.
-
+
diff --git a/e2etests/testdata/stable/md_2space_newline/dagre/sketch.exp.svg b/e2etests/testdata/stable/md_2space_newline/dagre/sketch.exp.svg
index 60c5b1afa..3223ce950 100644
--- a/e2etests/testdata/stable/md_2space_newline/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/md_2space_newline/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-npm i -g@forge/cliSet up anAtlassian siteView the helloworld appforgetunnelforgeloginforgecreateforgedeployforgeinstallHot reloadchanges?Step 1Step 2Step 3Step 4forgedeployโฌค Forge CLIโฌค Requiredโฌค Optional YesNo
-
+ .d2-29674933 .fill-N1{fill:#0A0F25;}
+ .d2-29674933 .fill-N2{fill:#676C7E;}
+ .d2-29674933 .fill-N3{fill:#9499AB;}
+ .d2-29674933 .fill-N4{fill:#CFD2DD;}
+ .d2-29674933 .fill-N5{fill:#DEE1EB;}
+ .d2-29674933 .fill-N6{fill:#EEF1F8;}
+ .d2-29674933 .fill-N7{fill:#FFFFFF;}
+ .d2-29674933 .fill-B1{fill:#0D32B2;}
+ .d2-29674933 .fill-B2{fill:#0D32B2;}
+ .d2-29674933 .fill-B3{fill:#E3E9FD;}
+ .d2-29674933 .fill-B4{fill:#E3E9FD;}
+ .d2-29674933 .fill-B5{fill:#EDF0FD;}
+ .d2-29674933 .fill-B6{fill:#F7F8FE;}
+ .d2-29674933 .fill-AA2{fill:#4A6FF3;}
+ .d2-29674933 .fill-AA4{fill:#EDF0FD;}
+ .d2-29674933 .fill-AA5{fill:#F7F8FE;}
+ .d2-29674933 .fill-AB4{fill:#EDF0FD;}
+ .d2-29674933 .fill-AB5{fill:#F7F8FE;}
+ .d2-29674933 .stroke-N1{stroke:#0A0F25;}
+ .d2-29674933 .stroke-N2{stroke:#676C7E;}
+ .d2-29674933 .stroke-N3{stroke:#9499AB;}
+ .d2-29674933 .stroke-N4{stroke:#CFD2DD;}
+ .d2-29674933 .stroke-N5{stroke:#DEE1EB;}
+ .d2-29674933 .stroke-N6{stroke:#EEF1F8;}
+ .d2-29674933 .stroke-N7{stroke:#FFFFFF;}
+ .d2-29674933 .stroke-B1{stroke:#0D32B2;}
+ .d2-29674933 .stroke-B2{stroke:#0D32B2;}
+ .d2-29674933 .stroke-B3{stroke:#E3E9FD;}
+ .d2-29674933 .stroke-B4{stroke:#E3E9FD;}
+ .d2-29674933 .stroke-B5{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-B6{stroke:#F7F8FE;}
+ .d2-29674933 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-29674933 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-29674933 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-29674933 .background-color-N1{background-color:#0A0F25;}
+ .d2-29674933 .background-color-N2{background-color:#676C7E;}
+ .d2-29674933 .background-color-N3{background-color:#9499AB;}
+ .d2-29674933 .background-color-N4{background-color:#CFD2DD;}
+ .d2-29674933 .background-color-N5{background-color:#DEE1EB;}
+ .d2-29674933 .background-color-N6{background-color:#EEF1F8;}
+ .d2-29674933 .background-color-N7{background-color:#FFFFFF;}
+ .d2-29674933 .background-color-B1{background-color:#0D32B2;}
+ .d2-29674933 .background-color-B2{background-color:#0D32B2;}
+ .d2-29674933 .background-color-B3{background-color:#E3E9FD;}
+ .d2-29674933 .background-color-B4{background-color:#E3E9FD;}
+ .d2-29674933 .background-color-B5{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-B6{background-color:#F7F8FE;}
+ .d2-29674933 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-29674933 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-29674933 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-29674933 .color-N1{color:#0A0F25;}
+ .d2-29674933 .color-N2{color:#676C7E;}
+ .d2-29674933 .color-N3{color:#9499AB;}
+ .d2-29674933 .color-N4{color:#CFD2DD;}
+ .d2-29674933 .color-N5{color:#DEE1EB;}
+ .d2-29674933 .color-N6{color:#EEF1F8;}
+ .d2-29674933 .color-N7{color:#FFFFFF;}
+ .d2-29674933 .color-B1{color:#0D32B2;}
+ .d2-29674933 .color-B2{color:#0D32B2;}
+ .d2-29674933 .color-B3{color:#E3E9FD;}
+ .d2-29674933 .color-B4{color:#E3E9FD;}
+ .d2-29674933 .color-B5{color:#EDF0FD;}
+ .d2-29674933 .color-B6{color:#F7F8FE;}
+ .d2-29674933 .color-AA2{color:#4A6FF3;}
+ .d2-29674933 .color-AA4{color:#EDF0FD;}
+ .d2-29674933 .color-AA5{color:#F7F8FE;}
+ .d2-29674933 .color-AB4{color:#EDF0FD;}
+ .d2-29674933 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>npm i -g@forge/cliSet up anAtlassian siteView the helloworld appforgetunnelforgeloginforgecreateforgedeployforgeinstallHot reloadchanges?Step 1Step 2Step 3Step 4forgedeployโฌค Forge CLIโฌค Requiredโฌค Optional YesNo
+
-
+
-
+
-
+
-
-
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/simple_grid_edges/elk/board.exp.json b/e2etests/testdata/stable/simple_grid_edges/elk/board.exp.json
index 3247aa247..52e639ce5 100644
--- a/e2etests/testdata/stable/simple_grid_edges/elk/board.exp.json
+++ b/e2etests/testdata/stable/simple_grid_edges/elk/board.exp.json
@@ -192,7 +192,7 @@
"x": 480,
"y": 0
},
- "width": 120,
+ "width": 100,
"height": 60,
"opacity": 1,
"strokeDash": 0,
@@ -408,7 +408,7 @@
"x": 480,
"y": 65
},
- "width": 120,
+ "width": 100,
"height": 30,
"opacity": 1,
"strokeDash": 0,
@@ -632,7 +632,7 @@
"x": 480,
"y": 100
},
- "width": 120,
+ "width": 100,
"height": 60,
"opacity": 1,
"strokeDash": 0,
@@ -852,7 +852,7 @@
"x": 480,
"y": 165
},
- "width": 120,
+ "width": 100,
"height": 30,
"opacity": 1,
"strokeDash": 0,
@@ -1188,7 +1188,7 @@
"x": 480,
"y": 200
},
- "width": 120,
+ "width": 100,
"height": 60,
"opacity": 1,
"strokeDash": 0,
@@ -1539,11 +1539,11 @@
"labelPercentage": 0,
"route": [
{
- "x": 540,
+ "x": 530,
"y": 100
},
{
- "x": 540,
+ "x": 530,
"y": 60
}
],
@@ -1580,11 +1580,11 @@
"labelPercentage": 0,
"route": [
{
- "x": 540,
+ "x": 530,
"y": 160
},
{
- "x": 540,
+ "x": 530,
"y": 200
}
],
diff --git a/e2etests/testdata/stable/simple_grid_edges/elk/sketch.exp.svg b/e2etests/testdata/stable/simple_grid_edges/elk/sketch.exp.svg
index a6635f2aa..c02c64467 100644
--- a/e2etests/testdata/stable/simple_grid_edges/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/simple_grid_edges/elk/sketch.exp.svg
@@ -1,23 +1,23 @@
-npm i -g@forge/cliSet up anAtlassian siteView the helloworld appforgetunnelforgeloginforgecreateforgedeployforgeinstallHot reloadchanges?Step 1Step 2Step 3Step 4forgedeployโฌค Forge CLIโฌค Requiredโฌค Optional YesNo
-
+ .d2-29674933 .fill-N1{fill:#0A0F25;}
+ .d2-29674933 .fill-N2{fill:#676C7E;}
+ .d2-29674933 .fill-N3{fill:#9499AB;}
+ .d2-29674933 .fill-N4{fill:#CFD2DD;}
+ .d2-29674933 .fill-N5{fill:#DEE1EB;}
+ .d2-29674933 .fill-N6{fill:#EEF1F8;}
+ .d2-29674933 .fill-N7{fill:#FFFFFF;}
+ .d2-29674933 .fill-B1{fill:#0D32B2;}
+ .d2-29674933 .fill-B2{fill:#0D32B2;}
+ .d2-29674933 .fill-B3{fill:#E3E9FD;}
+ .d2-29674933 .fill-B4{fill:#E3E9FD;}
+ .d2-29674933 .fill-B5{fill:#EDF0FD;}
+ .d2-29674933 .fill-B6{fill:#F7F8FE;}
+ .d2-29674933 .fill-AA2{fill:#4A6FF3;}
+ .d2-29674933 .fill-AA4{fill:#EDF0FD;}
+ .d2-29674933 .fill-AA5{fill:#F7F8FE;}
+ .d2-29674933 .fill-AB4{fill:#EDF0FD;}
+ .d2-29674933 .fill-AB5{fill:#F7F8FE;}
+ .d2-29674933 .stroke-N1{stroke:#0A0F25;}
+ .d2-29674933 .stroke-N2{stroke:#676C7E;}
+ .d2-29674933 .stroke-N3{stroke:#9499AB;}
+ .d2-29674933 .stroke-N4{stroke:#CFD2DD;}
+ .d2-29674933 .stroke-N5{stroke:#DEE1EB;}
+ .d2-29674933 .stroke-N6{stroke:#EEF1F8;}
+ .d2-29674933 .stroke-N7{stroke:#FFFFFF;}
+ .d2-29674933 .stroke-B1{stroke:#0D32B2;}
+ .d2-29674933 .stroke-B2{stroke:#0D32B2;}
+ .d2-29674933 .stroke-B3{stroke:#E3E9FD;}
+ .d2-29674933 .stroke-B4{stroke:#E3E9FD;}
+ .d2-29674933 .stroke-B5{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-B6{stroke:#F7F8FE;}
+ .d2-29674933 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-29674933 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-29674933 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-29674933 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-29674933 .background-color-N1{background-color:#0A0F25;}
+ .d2-29674933 .background-color-N2{background-color:#676C7E;}
+ .d2-29674933 .background-color-N3{background-color:#9499AB;}
+ .d2-29674933 .background-color-N4{background-color:#CFD2DD;}
+ .d2-29674933 .background-color-N5{background-color:#DEE1EB;}
+ .d2-29674933 .background-color-N6{background-color:#EEF1F8;}
+ .d2-29674933 .background-color-N7{background-color:#FFFFFF;}
+ .d2-29674933 .background-color-B1{background-color:#0D32B2;}
+ .d2-29674933 .background-color-B2{background-color:#0D32B2;}
+ .d2-29674933 .background-color-B3{background-color:#E3E9FD;}
+ .d2-29674933 .background-color-B4{background-color:#E3E9FD;}
+ .d2-29674933 .background-color-B5{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-B6{background-color:#F7F8FE;}
+ .d2-29674933 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-29674933 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-29674933 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-29674933 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-29674933 .color-N1{color:#0A0F25;}
+ .d2-29674933 .color-N2{color:#676C7E;}
+ .d2-29674933 .color-N3{color:#9499AB;}
+ .d2-29674933 .color-N4{color:#CFD2DD;}
+ .d2-29674933 .color-N5{color:#DEE1EB;}
+ .d2-29674933 .color-N6{color:#EEF1F8;}
+ .d2-29674933 .color-N7{color:#FFFFFF;}
+ .d2-29674933 .color-B1{color:#0D32B2;}
+ .d2-29674933 .color-B2{color:#0D32B2;}
+ .d2-29674933 .color-B3{color:#E3E9FD;}
+ .d2-29674933 .color-B4{color:#E3E9FD;}
+ .d2-29674933 .color-B5{color:#EDF0FD;}
+ .d2-29674933 .color-B6{color:#F7F8FE;}
+ .d2-29674933 .color-AA2{color:#4A6FF3;}
+ .d2-29674933 .color-AA4{color:#EDF0FD;}
+ .d2-29674933 .color-AA5{color:#F7F8FE;}
+ .d2-29674933 .color-AB4{color:#EDF0FD;}
+ .d2-29674933 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>npm i -g@forge/cliSet up anAtlassian siteView the helloworld appforgetunnelforgeloginforgecreateforgedeployforgeinstallHot reloadchanges?Step 1Step 2Step 3Step 4forgedeployโฌค Forge CLIโฌค Requiredโฌค Optional YesNo
+
-
+
-
+
-
+
-
-
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/sql_table_column_styles/dagre/sketch.exp.svg b/e2etests/testdata/stable/sql_table_column_styles/dagre/sketch.exp.svg
index d86edd450..d4c5c6a7c 100644
--- a/e2etests/testdata/stable/sql_table_column_styles/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/sql_table_column_styles/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-xyPKabFK I like turtles
+ .d2-3096218097 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles
diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg
index c27b67ada..698aafa98 100644
--- a/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/sql_table_tooltip_animated/elk/sketch.exp.svg
@@ -1,4 +1,4 @@
-xyPKabFK I like turtles
+ .d2-3579465052 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles
diff --git a/e2etests/testdata/stable/sql_tables/dagre/sketch.exp.svg b/e2etests/testdata/stable/sql_tables/dagre/sketch.exp.svg
index c58cb1e0c..7f9fb4b91 100644
--- a/e2etests/testdata/stable/sql_tables/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/sql_tables/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-abcdefgx
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/bidirectional-connection-animation/elk/board.exp.json b/e2etests/testdata/txtar/bidirectional-connection-animation/elk/board.exp.json
new file mode 100644
index 000000000..85d12fc27
--- /dev/null
+++ b/e2etests/testdata/txtar/bidirectional-connection-animation/elk/board.exp.json
@@ -0,0 +1,645 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 68,
+ "y": 12
+ },
+ "width": 160,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 208
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 85,
+ "y": 208
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 158,
+ "y": 208
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 232,
+ "y": 208
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 306,
+ "y": 12
+ },
+ "width": 51,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 6,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "g",
+ "type": "rectangle",
+ "pos": {
+ "x": 305,
+ "y": 208
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 427,
+ "y": 12
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a <-> b)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 100.25,
+ "y": 78
+ },
+ {
+ "x": 100.25,
+ "y": 118
+ },
+ {
+ "x": 38.5,
+ "y": 118
+ },
+ {
+ "x": 38.5,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> c)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 132.25,
+ "y": 78
+ },
+ {
+ "x": 132.25,
+ "y": 168
+ },
+ {
+ "x": 111.5,
+ "y": 168
+ },
+ {
+ "x": 111.5,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> d)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 164.25,
+ "y": 78
+ },
+ {
+ "x": 164.25,
+ "y": 168
+ },
+ {
+ "x": 185,
+ "y": 168
+ },
+ {
+ "x": 185,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> e)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "e",
+ "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,
+ "route": [
+ {
+ "x": 196.25,
+ "y": 78
+ },
+ {
+ "x": 196.25,
+ "y": 118
+ },
+ {
+ "x": 258.5,
+ "y": 118
+ },
+ {
+ "x": 258.5,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(f <-> g)[0]",
+ "src": "f",
+ "srcArrow": "triangle",
+ "dst": "g",
+ "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,
+ "route": [
+ {
+ "x": 332,
+ "y": 78
+ },
+ {
+ "x": 332,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x -- x)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "dst": "x",
+ "dstArrow": "none",
+ "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,
+ "route": [
+ {
+ "x": 427.5,
+ "y": 34
+ },
+ {
+ "x": 377.5,
+ "y": 34
+ },
+ {
+ "x": 377.5,
+ "y": 56
+ },
+ {
+ "x": 427.5,
+ "y": 56
+ }
+ ],
+ "animated": true,
+ "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": "",
+ "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/bidirectional-connection-animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional-connection-animation/elk/sketch.exp.svg
new file mode 100644
index 000000000..5bdfdd2c5
--- /dev/null
+++ b/e2etests/testdata/txtar/bidirectional-connection-animation/elk/sketch.exp.svg
@@ -0,0 +1,108 @@
+abcdefgx
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/connection-underline/dagre/board.exp.json b/e2etests/testdata/txtar/connection-underline/dagre/board.exp.json
new file mode 100644
index 000000000..93d7d24b3
--- /dev/null
+++ b/e2etests/testdata/txtar/connection-underline/dagre/board.exp.json
@@ -0,0 +1,178 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 187
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "hi",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": true,
+ "labelWidth": 13,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 26.5,
+ "y": 65.5
+ },
+ {
+ "x": 26.5,
+ "y": 114.30000305175781
+ },
+ {
+ "x": 26.5,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 26.5,
+ "y": 187.5
+ }
+ ],
+ "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": "",
+ "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/connection-underline/dagre/sketch.exp.svg b/e2etests/testdata/txtar/connection-underline/dagre/sketch.exp.svg
new file mode 100644
index 000000000..96cee21d5
--- /dev/null
+++ b/e2etests/testdata/txtar/connection-underline/dagre/sketch.exp.svg
@@ -0,0 +1,107 @@
+ab hi
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/connection-underline/elk/board.exp.json b/e2etests/testdata/txtar/connection-underline/elk/board.exp.json
new file mode 100644
index 000000000..a1cde3b44
--- /dev/null
+++ b/e2etests/testdata/txtar/connection-underline/elk/board.exp.json
@@ -0,0 +1,169 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 239
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "hi",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": true,
+ "labelWidth": 13,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 38.5,
+ "y": 78
+ },
+ {
+ "x": 38.5,
+ "y": 239
+ }
+ ],
+ "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": "",
+ "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/connection-underline/elk/sketch.exp.svg b/e2etests/testdata/txtar/connection-underline/elk/sketch.exp.svg
new file mode 100644
index 000000000..2c1399ced
--- /dev/null
+++ b/e2etests/testdata/txtar/connection-underline/elk/sketch.exp.svg
@@ -0,0 +1,107 @@
+ab hi
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/nested-spread-substitutions-regression/dagre/board.exp.json b/e2etests/testdata/txtar/nested-spread-substitutions-regression/dagre/board.exp.json
new file mode 100644
index 000000000..648e0b3f2
--- /dev/null
+++ b/e2etests/testdata/txtar/nested-spread-substitutions-regression/dagre/board.exp.json
@@ -0,0 +1,219 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "ok",
+ "type": "rectangle",
+ "pos": {
+ "x": 10,
+ "y": 20
+ },
+ "width": 157,
+ "height": 323,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Home",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "ok.dog1",
+ "type": "oval",
+ "pos": {
+ "x": 40,
+ "y": 50
+ },
+ "width": 97,
+ "height": 97,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "dog1",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 35,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "ok.dog3",
+ "type": "rectangle",
+ "pos": {
+ "x": 49,
+ "y": 247
+ },
+ "width": 80,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "dog3",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 35,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "ok.(dog1 -> dog3)[0]",
+ "src": "ok.dog1",
+ "srcArrow": "none",
+ "dst": "ok.dog3",
+ "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,
+ "route": [
+ {
+ "x": 89,
+ "y": 147
+ },
+ {
+ "x": 88.5999984741211,
+ "y": 187
+ },
+ {
+ "x": 88.5,
+ "y": 207
+ },
+ {
+ "x": 88.5,
+ "y": 247
+ }
+ ],
+ "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": "",
+ "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/nested-spread-substitutions-regression/dagre/sketch.exp.svg b/e2etests/testdata/txtar/nested-spread-substitutions-regression/dagre/sketch.exp.svg
new file mode 100644
index 000000000..21cd18a74
--- /dev/null
+++ b/e2etests/testdata/txtar/nested-spread-substitutions-regression/dagre/sketch.exp.svg
@@ -0,0 +1,104 @@
+Homedog1dog3
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/nested-spread-substitutions-regression/elk/board.exp.json b/e2etests/testdata/txtar/nested-spread-substitutions-regression/elk/board.exp.json
new file mode 100644
index 000000000..6505afcec
--- /dev/null
+++ b/e2etests/testdata/txtar/nested-spread-substitutions-regression/elk/board.exp.json
@@ -0,0 +1,210 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "ok",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 197,
+ "height": 333,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Home",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 68,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "ok.dog1",
+ "type": "oval",
+ "pos": {
+ "x": 62,
+ "y": 62
+ },
+ "width": 97,
+ "height": 97,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "dog1",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 35,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "ok.dog3",
+ "type": "rectangle",
+ "pos": {
+ "x": 70,
+ "y": 229
+ },
+ "width": 80,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "dog3",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 35,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "ok.(dog1 -> dog3)[0]",
+ "src": "ok.dog1",
+ "srcArrow": "none",
+ "dst": "ok.dog3",
+ "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,
+ "route": [
+ {
+ "x": 111,
+ "y": 159
+ },
+ {
+ "x": 110,
+ "y": 229
+ }
+ ],
+ "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": "",
+ "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/nested-spread-substitutions-regression/elk/sketch.exp.svg b/e2etests/testdata/txtar/nested-spread-substitutions-regression/elk/sketch.exp.svg
new file mode 100644
index 000000000..5a91dc8bb
--- /dev/null
+++ b/e2etests/testdata/txtar/nested-spread-substitutions-regression/elk/sketch.exp.svg
@@ -0,0 +1,104 @@
+Homedog1dog3
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/none-fill/dagre/board.exp.json b/e2etests/testdata/txtar/none-fill/dagre/board.exp.json
new file mode 100644
index 000000000..af4ed577d
--- /dev/null
+++ b/e2etests/testdata/txtar/none-fill/dagre/board.exp.json
@@ -0,0 +1,140 @@
+{
+ "name": "",
+ "config": {
+ "sketch": null,
+ "themeID": 302,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "fillPattern": "none",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 113,
+ "y": 0
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "fillPattern": "paper",
+ "stroke": "B1",
+ "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": 1
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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/none-fill/dagre/sketch.exp.svg b/e2etests/testdata/txtar/none-fill/dagre/sketch.exp.svg
new file mode 100644
index 000000000..96711aaa3
--- /dev/null
+++ b/e2etests/testdata/txtar/none-fill/dagre/sketch.exp.svg
@@ -0,0 +1,1160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ab
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/none-fill/elk/board.exp.json b/e2etests/testdata/txtar/none-fill/elk/board.exp.json
new file mode 100644
index 000000000..444981e85
--- /dev/null
+++ b/e2etests/testdata/txtar/none-fill/elk/board.exp.json
@@ -0,0 +1,140 @@
+{
+ "name": "",
+ "config": {
+ "sketch": null,
+ "themeID": 302,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "fillPattern": "none",
+ "stroke": "B1",
+ "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": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 85,
+ "y": 12
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "fillPattern": "paper",
+ "stroke": "B1",
+ "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": 1
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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/none-fill/elk/sketch.exp.svg b/e2etests/testdata/txtar/none-fill/elk/sketch.exp.svg
new file mode 100644
index 000000000..fb9934b9d
--- /dev/null
+++ b/e2etests/testdata/txtar/none-fill/elk/sketch.exp.svg
@@ -0,0 +1,1160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ab
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/opacity-zero-route/dagre/board.exp.json b/e2etests/testdata/txtar/opacity-zero-route/dagre/board.exp.json
new file mode 100644
index 000000000..c4f256797
--- /dev/null
+++ b/e2etests/testdata/txtar/opacity-zero-route/dagre/board.exp.json
@@ -0,0 +1,497 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "grid",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 361,
+ "height": 398,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "grid",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "grid.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 60,
+ "y": 60
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 60,
+ "y": 166
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 60,
+ "y": 272
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 153,
+ "y": 60
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.e",
+ "type": "rectangle",
+ "pos": {
+ "x": 153,
+ "y": 166
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "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": "grid.f",
+ "type": "rectangle",
+ "pos": {
+ "x": 153,
+ "y": 272
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 6,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.g",
+ "type": "rectangle",
+ "pos": {
+ "x": 247,
+ "y": 60
+ },
+ "width": 54,
+ "height": 119,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "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": "grid.h",
+ "type": "rectangle",
+ "pos": {
+ "x": 247,
+ "y": 219
+ },
+ "width": 54,
+ "height": 119,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "h",
+ "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": "out",
+ "type": "rectangle",
+ "pos": {
+ "x": 421,
+ "y": 166
+ },
+ "width": 69,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "out",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 24,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(out -> grid.d)[0]",
+ "src": "out",
+ "srcArrow": "none",
+ "dst": "grid.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,
+ "route": [
+ {
+ "x": 421,
+ "y": 186
+ },
+ {
+ "x": 207,
+ "y": 103
+ }
+ ],
+ "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": "",
+ "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/opacity-zero-route/dagre/sketch.exp.svg b/e2etests/testdata/txtar/opacity-zero-route/dagre/sketch.exp.svg
new file mode 100644
index 000000000..a30ccdea9
--- /dev/null
+++ b/e2etests/testdata/txtar/opacity-zero-route/dagre/sketch.exp.svg
@@ -0,0 +1,104 @@
+gridoutd
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/opacity-zero-route/elk/board.exp.json b/e2etests/testdata/txtar/opacity-zero-route/elk/board.exp.json
new file mode 100644
index 000000000..583020b29
--- /dev/null
+++ b/e2etests/testdata/txtar/opacity-zero-route/elk/board.exp.json
@@ -0,0 +1,497 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "grid",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 361,
+ "height": 398,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "grid",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "grid.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 72,
+ "y": 72
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 72,
+ "y": 178
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 72,
+ "y": 284
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 165,
+ "y": 72
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "grid.e",
+ "type": "rectangle",
+ "pos": {
+ "x": 165,
+ "y": 178
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "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": "grid.f",
+ "type": "rectangle",
+ "pos": {
+ "x": 165,
+ "y": 284
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 6,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.g",
+ "type": "rectangle",
+ "pos": {
+ "x": 259,
+ "y": 72
+ },
+ "width": 54,
+ "height": 119,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "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": "grid.h",
+ "type": "rectangle",
+ "pos": {
+ "x": 259,
+ "y": 231
+ },
+ "width": 54,
+ "height": 119,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "h",
+ "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": "out",
+ "type": "rectangle",
+ "pos": {
+ "x": 393,
+ "y": 178
+ },
+ "width": 69,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "out",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 24,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(out -> grid.d)[0]",
+ "src": "out",
+ "srcArrow": "none",
+ "dst": "grid.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,
+ "route": [
+ {
+ "x": 393,
+ "y": 195
+ },
+ {
+ "x": 219,
+ "y": 117
+ }
+ ],
+ "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": "",
+ "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/opacity-zero-route/elk/sketch.exp.svg b/e2etests/testdata/txtar/opacity-zero-route/elk/sketch.exp.svg
new file mode 100644
index 000000000..b7c4dcdf6
--- /dev/null
+++ b/e2etests/testdata/txtar/opacity-zero-route/elk/sketch.exp.svg
@@ -0,0 +1,104 @@
+gridoutd
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/dagre/board.exp.json b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/dagre/board.exp.json
new file mode 100644
index 000000000..d4be34b71
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/dagre/board.exp.json
@@ -0,0 +1,703 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": null,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 172,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 166
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 115,
+ "y": 166
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 229,
+ "y": 166
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 344,
+ "y": 166
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 457,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "g",
+ "type": "rectangle",
+ "pos": {
+ "x": 457,
+ "y": 166
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 571,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a <-> b)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 172.5,
+ "y": 46
+ },
+ {
+ "x": 56.5,
+ "y": 102
+ },
+ {
+ "x": 27.5,
+ "y": 126
+ },
+ {
+ "x": 27.5,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> c)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 176,
+ "y": 66
+ },
+ {
+ "x": 148.8000030517578,
+ "y": 106
+ },
+ {
+ "x": 142,
+ "y": 126
+ },
+ {
+ "x": 142,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> d)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 222.5,
+ "y": 66
+ },
+ {
+ "x": 249.6999969482422,
+ "y": 106
+ },
+ {
+ "x": 256.5,
+ "y": 126
+ },
+ {
+ "x": 256.5,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> e)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "e",
+ "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,
+ "route": [
+ {
+ "x": 226.25,
+ "y": 46.08599853515625
+ },
+ {
+ "x": 341.6499938964844,
+ "y": 102.01699829101562
+ },
+ {
+ "x": 370.5,
+ "y": 126
+ },
+ {
+ "x": 370.5,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(f <-> g)[0]",
+ "src": "f",
+ "srcArrow": "triangle",
+ "dst": "g",
+ "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,
+ "route": [
+ {
+ "x": 484,
+ "y": 66
+ },
+ {
+ "x": 484,
+ "y": 106
+ },
+ {
+ "x": 484,
+ "y": 126
+ },
+ {
+ "x": 484,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x -- x)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "dst": "x",
+ "dstArrow": "none",
+ "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,
+ "route": [
+ {
+ "x": 624.666015625,
+ "y": 16
+ },
+ {
+ "x": 646.2659912109375,
+ "y": 3.1989998817443848
+ },
+ {
+ "x": 653,
+ "y": 0
+ },
+ {
+ "x": 655,
+ "y": 0
+ },
+ {
+ "x": 657,
+ "y": 0
+ },
+ {
+ "x": 659.666015625,
+ "y": 6.599999904632568
+ },
+ {
+ "x": 661.666015625,
+ "y": 16.5
+ },
+ {
+ "x": 663.666015625,
+ "y": 26.399999618530273
+ },
+ {
+ "x": 663.666015625,
+ "y": 39.599998474121094
+ },
+ {
+ "x": 661.666015625,
+ "y": 49.5
+ },
+ {
+ "x": 659.666015625,
+ "y": 59.400001525878906
+ },
+ {
+ "x": 646.2659912109375,
+ "y": 62.79999923706055
+ },
+ {
+ "x": 624.666015625,
+ "y": 50
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "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": "",
+ "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/sketch-bidirectional-connection-animation/dagre/sketch.exp.svg b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/dagre/sketch.exp.svg
new file mode 100644
index 000000000..01ae7f387
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/dagre/sketch.exp.svg
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+abcdefgx
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/elk/board.exp.json b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/elk/board.exp.json
new file mode 100644
index 000000000..5abaaf205
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/elk/board.exp.json
@@ -0,0 +1,653 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": null,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 71,
+ "y": 12
+ },
+ "width": 160,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 208
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 87,
+ "y": 208
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 161,
+ "y": 208
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 236,
+ "y": 208
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 309,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "g",
+ "type": "rectangle",
+ "pos": {
+ "x": 309,
+ "y": 208
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 433,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a <-> b)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 103.25,
+ "y": 78
+ },
+ {
+ "x": 103.25,
+ "y": 118
+ },
+ {
+ "x": 39.5,
+ "y": 118
+ },
+ {
+ "x": 39.5,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> c)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 135.25,
+ "y": 78
+ },
+ {
+ "x": 135.25,
+ "y": 168
+ },
+ {
+ "x": 114,
+ "y": 168
+ },
+ {
+ "x": 114,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> d)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 167.25,
+ "y": 78
+ },
+ {
+ "x": 167.25,
+ "y": 168
+ },
+ {
+ "x": 188.5,
+ "y": 168
+ },
+ {
+ "x": 188.5,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> e)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "e",
+ "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,
+ "route": [
+ {
+ "x": 199.25,
+ "y": 78
+ },
+ {
+ "x": 199.25,
+ "y": 118
+ },
+ {
+ "x": 262.5,
+ "y": 118
+ },
+ {
+ "x": 262.5,
+ "y": 208
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(f <-> g)[0]",
+ "src": "f",
+ "srcArrow": "triangle",
+ "dst": "g",
+ "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,
+ "route": [
+ {
+ "x": 336,
+ "y": 78
+ },
+ {
+ "x": 336,
+ "y": 208
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x -- x)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "dst": "x",
+ "dstArrow": "none",
+ "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,
+ "route": [
+ {
+ "x": 433,
+ "y": 34
+ },
+ {
+ "x": 383,
+ "y": 34
+ },
+ {
+ "x": 383,
+ "y": 56
+ },
+ {
+ "x": 433,
+ "y": 56
+ }
+ ],
+ "animated": true,
+ "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": "",
+ "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/sketch-bidirectional-connection-animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/elk/sketch.exp.svg
new file mode 100644
index 000000000..9603d3358
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-bidirectional-connection-animation/elk/sketch.exp.svg
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+abcdefgx
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/dagre/board.exp.json b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/dagre/board.exp.json
new file mode 100644
index 000000000..6e64ad35a
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/dagre/board.exp.json
@@ -0,0 +1,403 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": null,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 1,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 166
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 115,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "y",
+ "type": "rectangle",
+ "pos": {
+ "x": 115,
+ "y": 166
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "y",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "z",
+ "type": "rectangle",
+ "pos": {
+ "x": 116,
+ "y": 332
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "z",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a <-> b)[0]",
+ "src": "a",
+ "srcArrow": "circle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 27.5,
+ "y": 66
+ },
+ {
+ "x": 27.5,
+ "y": 106
+ },
+ {
+ "x": 27.5,
+ "y": 126
+ },
+ {
+ "x": 27.5,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x <-> y)[0]",
+ "src": "x",
+ "srcArrow": "circle",
+ "dst": "y",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 142,
+ "y": 66
+ },
+ {
+ "x": 142,
+ "y": 106
+ },
+ {
+ "x": 142,
+ "y": 126
+ },
+ {
+ "x": 142,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(y <-> z)[0]",
+ "src": "y",
+ "srcArrow": "circle",
+ "dst": "z",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 142,
+ "y": 232
+ },
+ {
+ "x": 142,
+ "y": 272
+ },
+ {
+ "x": 142,
+ "y": 292
+ },
+ {
+ "x": 142,
+ "y": 332
+ }
+ ],
+ "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": "",
+ "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/sketch-mode-circle-arrowhead/dagre/sketch.exp.svg b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/dagre/sketch.exp.svg
new file mode 100644
index 000000000..cc3a74131
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/dagre/sketch.exp.svg
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+abxyz
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/elk/board.exp.json b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/elk/board.exp.json
new file mode 100644
index 000000000..aeccd4631
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/elk/board.exp.json
@@ -0,0 +1,376 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": null,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 148
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 87,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "y",
+ "type": "rectangle",
+ "pos": {
+ "x": 87,
+ "y": 148
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "y",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "z",
+ "type": "rectangle",
+ "pos": {
+ "x": 87,
+ "y": 284
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "z",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a <-> b)[0]",
+ "src": "a",
+ "srcArrow": "circle",
+ "dst": "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,
+ "route": [
+ {
+ "x": 39.5,
+ "y": 78
+ },
+ {
+ "x": 39.5,
+ "y": 148
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x <-> y)[0]",
+ "src": "x",
+ "srcArrow": "circle",
+ "dst": "y",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 114,
+ "y": 78
+ },
+ {
+ "x": 114,
+ "y": 148
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(y <-> z)[0]",
+ "src": "y",
+ "srcArrow": "circle",
+ "dst": "z",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 114,
+ "y": 214
+ },
+ {
+ "x": 114,
+ "y": 284
+ }
+ ],
+ "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": "",
+ "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/sketch-mode-circle-arrowhead/elk/sketch.exp.svg b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/elk/sketch.exp.svg
new file mode 100644
index 000000000..ccf71925b
--- /dev/null
+++ b/e2etests/testdata/txtar/sketch-mode-circle-arrowhead/elk/sketch.exp.svg
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+abxyz
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/sql-icon/dagre/sketch.exp.svg b/e2etests/testdata/txtar/sql-icon/dagre/sketch.exp.svg
index b10457082..5919e55ac 100644
--- a/e2etests/testdata/txtar/sql-icon/dagre/sketch.exp.svg
+++ b/e2etests/testdata/txtar/sql-icon/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-long label
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/width-smaller-than-label-custom-pos/elk/board.exp.json b/e2etests/testdata/txtar/width-smaller-than-label-custom-pos/elk/board.exp.json
new file mode 100644
index 000000000..b9a0cbcde
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label-custom-pos/elk/board.exp.json
@@ -0,0 +1,89 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 20,
+ "height": 61,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "long label",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 69,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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/width-smaller-than-label-custom-pos/elk/sketch.exp.svg b/e2etests/testdata/txtar/width-smaller-than-label-custom-pos/elk/sketch.exp.svg
new file mode 100644
index 000000000..6acf351ec
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label-custom-pos/elk/sketch.exp.svg
@@ -0,0 +1,95 @@
+long label
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/width-smaller-than-label/dagre/board.exp.json b/e2etests/testdata/txtar/width-smaller-than-label/dagre/board.exp.json
new file mode 100644
index 000000000..e7a9496a4
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label/dagre/board.exp.json
@@ -0,0 +1,89 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "b",
+ "type": "person",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 64,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B3",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hello there cat",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 101,
+ "labelHeight": 21,
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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/width-smaller-than-label/dagre/sketch.exp.svg b/e2etests/testdata/txtar/width-smaller-than-label/dagre/sketch.exp.svg
new file mode 100644
index 000000000..2dfd70484
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label/dagre/sketch.exp.svg
@@ -0,0 +1,95 @@
+hello there cat
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/width-smaller-than-label/elk/board.exp.json b/e2etests/testdata/txtar/width-smaller-than-label/elk/board.exp.json
new file mode 100644
index 000000000..605a1b81b
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label/elk/board.exp.json
@@ -0,0 +1,89 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "b",
+ "type": "person",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 64,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B3",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hello there cat",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 101,
+ "labelHeight": 21,
+ "labelPosition": "OUTSIDE_BOTTOM_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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/width-smaller-than-label/elk/sketch.exp.svg b/e2etests/testdata/txtar/width-smaller-than-label/elk/sketch.exp.svg
new file mode 100644
index 000000000..eedaa8d73
--- /dev/null
+++ b/e2etests/testdata/txtar/width-smaller-than-label/elk/sketch.exp.svg
@@ -0,0 +1,95 @@
+hello there cat
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/chinese/dagre/sketch.exp.svg b/e2etests/testdata/unicode/chinese/dagre/sketch.exp.svg
index e3862efc1..27d1bf2a2 100644
--- a/e2etests/testdata/unicode/chinese/dagre/sketch.exp.svg
+++ b/e2etests/testdata/unicode/chinese/dagre/sketch.exp.svg
@@ -1,4 +1,4 @@
-
-`, svgURL, pngURL)
+`
+ sampleSVG := fmt.Sprintf(template, svgURL, pngURL)
l := simplelog.FromLibLog(ctx)
- out, err := BundleLocal(ctx, l, []byte(sampleSVG), false)
+ // It doesn't matter what the inputPath is for absolute paths
+ out, err := BundleLocal(ctx, l, "asdf", []byte(sampleSVG), false)
if err != nil {
t.Fatal(err)
}
@@ -208,6 +211,47 @@ width="328" height="587" viewBox="-100 -131 328 587">