Merge branch 'terrastruct:master' into master

This commit is contained in:
satoqz 2023-06-03 13:57:52 +02:00 committed by GitHub
commit 78995c38bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
924 changed files with 111339 additions and 25899 deletions

View file

@ -228,6 +228,7 @@ let us know and we'll be happy to include it here!
- **Mongo to D2**: [https://github.com/novuhq/mongo-to-D2](https://github.com/novuhq/mongo-to-D2)
- **Pandoc filter**: [https://github.com/ram02z/d2-filter](https://github.com/ram02z/d2-filter)
- **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/b-yp/logseq-d2)
### Misc
@ -275,5 +276,3 @@ this selected list of featured projects using D2.
- Cloud service emulator (46k stars)
- [Queue Library](https://github.com/golang-queue/queue/tree/master/images)
- Queue is a Golang library for spawning and managing a Goroutine pool
- [ent2d2](https://github.com/tmc/ent2d2)
- A project to render entity relation (ER) diagrams for the [Ent](https://entgo.io) ORM.

View file

@ -1,22 +1,22 @@
#### Features 🚀
- `class` field now accepts arrays. See [docs](TODO). [#1256](https://github.com/terrastruct/d2/pull/1256)
- Pill shape is implemented with rectangles of large border radius. Thanks @Poivey ! [#1006](https://github.com/terrastruct/d2/pull/1006)
#### Improvements 🧹
- ELK self loops get distributed around the object instead of stacking [#1232](https://github.com/terrastruct/d2/pull/1232)
- ELK preserves order of objects in cycles [#1235](https://github.com/terrastruct/d2/pull/1235)
- Improper usages of `class` and `style` get error messages [#1254](https://github.com/terrastruct/d2/pull/1254)
- Improves scaling of object widths/heights in grid diagrams [#1263](https://github.com/terrastruct/d2/pull/1263)
- Enhance Markdown parsing error message by appending link to docs [#1269](https://github.com/terrastruct/d2/pull/1269)
- Use shape specific sizing for grid containers [#1294](https://github.com/terrastruct/d2/pull/1294)
- Grid diagrams now support nested shapes or grid diagrams [#1309](https://github.com/terrastruct/d2/pull/1309)
- Grid diagrams will now also use `grid-gap`, `vertical-gap`, and `horizontal-gap` for padding [#1309](https://github.com/terrastruct/d2/pull/1309)
- Watch mode browser uses an error favicon to easily indicate compiler errors. Thanks @sinyo-matu ! [#1240](https://github.com/terrastruct/d2/pull/1240)
- Improves grid layout performance when there are many similarly sized shapes. [#1315](https://github.com/terrastruct/d2/pull/1315)
- Connections and labels now are adjusted for shapes with `3d` or `multiple`. [#1340](https://github.com/terrastruct/d2/pull/1340)
#### Bugfixes ⛑️
- Fixes an issue with markdown labels that are empty when rendered [#1223](https://github.com/terrastruct/d2/issues/1223)
- ELK self loops always have enough space for long labels [#1232](https://github.com/terrastruct/d2/pull/1232)
- Fixes panic when setting `shape` to be `class` or `sql_table` within a class [#1251](https://github.com/terrastruct/d2/pull/1251)
- Fixes rare panic exporting to gifs [#1257](https://github.com/terrastruct/d2/pull/1257)
- Fixes bad performance in large grid diagrams [#1263](https://github.com/terrastruct/d2/pull/1263)
- Fixes bug in ELK when container has ID "root" [#1268](https://github.com/terrastruct/d2/pull/1268)
- Fixes edge case panic with invalid CLI arguments [#1271](https://github.com/terrastruct/d2/pull/1271)
- Shadow is cut off when `--pad` is 0. Thank you @LeonardsonCC ! [#1326](https://github.com/terrastruct/d2/pull/1326)
- Fixes grid layout overwriting label placements for nested objects. [#1345](https://github.com/terrastruct/d2/pull/1345)
- Fixes fonts not rendering correctly on certain platforms. Thanks @mikeday for identifying the solution. [#1356](https://github.com/terrastruct/d2/pull/1356)
- Fixes folders not rendering in animations (`--animate-interval`) [#1357](https://github.com/terrastruct/d2/pull/1357)
- Fixes panic using reserved keywords as containers [#1358](https://github.com/terrastruct/d2/pull/1358)
- When multiple classes are applied changing different attributes of arrowheads, they are
all applied instead of only the last one [#1362](https://github.com/terrastruct/d2/pull/1362)
- Prevent empty block strings [#1364](https://github.com/terrastruct/d2/pull/1364)
- Fixes dagre mis-aligning a nested shape's connection. [#1370](https://github.com/terrastruct/d2/pull/1370)

View file

@ -0,0 +1,55 @@
This release improves on the features introduced in 0.4, with `class` keyword now accepting multiple class values with an array, and grid diagrams becoming faster and more robust.
Multiple classes example:
<img src="https://user-images.githubusercontent.com/3120367/235749202-aa85830e-8f4a-4a2c-be16-599302919122.svg" style="width: 600px" />
```d2
classes: {
base: {
style: {
stroke-dash: 2
border-radius: 5
font: mono
text-transform: uppercase
}
}
error: {
style.fill: "#e07d7d"
style.stroke: "#a60c0c"
style.font-color: white
}
success: {
style.fill: "#86f499"
style.stroke: "#017f07"
style.font-color: black
}
}
server-1.class: [base; error]
server-2.class: [base; success]
```
#### Features 🚀
- `class` field now accepts arrays. See [docs](https://d2lang.com/tour/classes/#multiple-classes). [#1256](https://github.com/terrastruct/d2/pull/1256)
- Pill shape is implemented with rectangles of large border radius. See [docs](https://d2lang.com/tour/style/#border-radius). Thanks @Poivey ! [#1006](https://github.com/terrastruct/d2/pull/1006)
#### Improvements 🧹
- ELK self loops get distributed around the object instead of stacking [#1232](https://github.com/terrastruct/d2/pull/1232)
- ELK preserves order of objects in cycles [#1235](https://github.com/terrastruct/d2/pull/1235)
- Improper usages of `class` and `style` get error messages [#1254](https://github.com/terrastruct/d2/pull/1254)
- Improves scaling of object widths/heights in grid diagrams [#1263](https://github.com/terrastruct/d2/pull/1263)
- Enhance Markdown parsing error message by appending link to docs [#1269](https://github.com/terrastruct/d2/pull/1269)
#### Bugfixes ⛑️
- Fixes an issue with markdown labels that are empty when rendered [#1223](https://github.com/terrastruct/d2/issues/1223)
- ELK self loops always have enough space for long labels [#1232](https://github.com/terrastruct/d2/pull/1232)
- Fixes panic when setting `shape` to be `class` or `sql_table` within a class [#1251](https://github.com/terrastruct/d2/pull/1251)
- Fixes rare panic exporting to gifs [#1257](https://github.com/terrastruct/d2/pull/1257)
- Fixes bad performance in large grid diagrams [#1263](https://github.com/terrastruct/d2/pull/1263)
- Fixes bug in ELK when container has ID "root" [#1268](https://github.com/terrastruct/d2/pull/1268)
- Fixes edge case panic with invalid CLI arguments [#1271](https://github.com/terrastruct/d2/pull/1271)

View file

@ -611,9 +611,13 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
return false
}
if (mk1.Value.Map == nil) != (mk2.Value.Map == nil) {
return false
}
if (mk1.Value.Unbox() == nil) != (mk2.Value.Unbox() == nil) {
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
return false
}
if mk2.Value.Map != nil && len(mk2.Value.Map.Nodes) > 0 {
return false
}
} else if (mk1.Value.Unbox() == nil) != (mk2.Value.Unbox() == nil) {
return false
}
@ -638,7 +642,7 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
}
}
if mk1.Value.Map != nil {
if mk1.Value.Map != nil && len(mk1.Value.Map.Nodes) > 0 {
if len(mk1.Value.Map.Nodes) != len(mk2.Value.Map.Nodes) {
return false
}

View file

@ -641,10 +641,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
}
boards = append([][]byte{out}, boards...)
return boards, nil
}
return nil, nil
return boards, 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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
d2cli/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -28,7 +28,7 @@ function init(reconnectDelay) {
// we can't just set `d2SVG.innerHTML = msg.svg` need to parse this as xml not html
const parsedXML = new DOMParser().parseFromString(msg.svg, "text/xml");
d2SVG.replaceChildren(parsedXML.documentElement);
changeFavicon("./static/favicon.ico");
const svgEl = d2SVG.querySelector("#d2-svg");
// just use inner SVG in watch mode
svgEl.parentElement.replaceWith(svgEl);
@ -56,6 +56,7 @@ function init(reconnectDelay) {
if (msg.err) {
d2ErrDiv.innerText = msg.err;
d2ErrDiv.style.display = "block";
changeFavicon("./static/favicon-err.ico");
d2ErrDiv.scrollIntoView();
}
};
@ -73,3 +74,8 @@ function init(reconnectDelay) {
}, reconnectDelay);
};
}
const changeFavicon = function (iconURL) {
const faviconLink = document.getElementById("favicon");
faviconLink.href = iconURL;
};

View file

@ -429,6 +429,7 @@ func (w *watcher) handleRoot(hw http.ResponseWriter, r *http.Request) {
<title>%s</title>
<script src="./static/watch.js"></script>
<link rel="stylesheet" href="./static/watch.css">
<link id="favicon" rel="icon" href="./static/favicon.ico">
</head>
<body data-d2-dev-mode=%t>
<div id="d2-err" style="display: none"></div>

View file

@ -210,8 +210,8 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
c.compileReserved(&obj.Attributes, f)
return
} else if f.Name == "style" {
if f.Map() == nil {
c.errorf(f.LastRef().AST(), `"style" expected to be set to a map, or contain an additional keyword like "style.opacity: 0.4"`)
if f.Map() == nil || len(f.Map().Fields) == 0 {
c.errorf(f.LastRef().AST(), `"style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`)
return
}
c.compileStyle(&obj.Attributes, f.Map())
@ -270,6 +270,9 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
// TODO: Delete instead.
attrs.Label.Value = scalar.ScalarString()
case *d2ast.BlockString:
if strings.TrimSpace(scalar.ScalarString()) == "" {
c.errorf(f.LastPrimaryKey(), "block string cannot be empty")
}
attrs.Language = scalar.Tag
fullTag, ok := ShortToFullLanguageAliases[scalar.Tag]
if ok {
@ -675,10 +678,14 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
var attrs *d2graph.Attributes
if f.Name == "source-arrowhead" {
edge.SrcArrowhead = &d2graph.Attributes{}
if edge.SrcArrowhead == nil {
edge.SrcArrowhead = &d2graph.Attributes{}
}
attrs = edge.SrcArrowhead
} else {
edge.DstArrowhead = &d2graph.Attributes{}
if edge.DstArrowhead == nil {
edge.DstArrowhead = &d2graph.Attributes{}
}
attrs = edge.DstArrowhead
}
@ -847,13 +854,6 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
if !in && arrowheadIn {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Shape.Value))
}
case "grid-rows", "grid-columns", "grid-gap", "vertical-gap", "horizontal-gap":
for _, child := range obj.ChildrenArray {
if child.IsContainer() {
c.errorf(f.LastPrimaryKey(),
fmt.Sprintf(`%#v can only be used on containers with one level of nesting right now. (%#v has nested %#v)`, keyword, child.AbsID(), child.ChildrenArray[0].ID))
}
}
}
return
}

View file

@ -610,6 +610,24 @@ x: {
expErr: `d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:4:19: unexpected text after md block string. See https://d2lang.com/tour/text#advanced-block-strings.
d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:5:1: block string must be terminated with |`,
},
{
name: "no_empty_block_string",
text: `Text: |md |`,
expErr: `d2/testdata/d2compiler/TestCompile/no_empty_block_string.d2:1:1: block string cannot be empty`,
},
{
name: "no_white_spaces_only_block_string",
text: `Text: |md |`,
expErr: `d2/testdata/d2compiler/TestCompile/no_white_spaces_only_block_string.d2:1:1: block string cannot be empty`,
},
{
name: "no_new_lines_only_block_string",
text: `Text: |md
|`,
expErr: `d2/testdata/d2compiler/TestCompile/no_new_lines_only_block_string.d2:1:1: block string cannot be empty`,
},
{
name: "underscore_edge_existing",
@ -1700,7 +1718,13 @@ x.a.b`,
name: "tail-style",
text: `myobj.style: 3`,
expErr: `d2/testdata/d2compiler/TestCompile/tail-style.d2:1:7: "style" expected to be set to a map, or contain an additional keyword like "style.opacity: 0.4"`,
expErr: `d2/testdata/d2compiler/TestCompile/tail-style.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
},
{
name: "tail-style-map",
text: `myobj.style: {}`,
expErr: `d2/testdata/d2compiler/TestCompile/tail-style-map.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
},
{
name: "bad-style-nesting",
@ -1716,6 +1740,13 @@ y -> x.style
`,
expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:8: reserved keywords are prohibited in edges`,
},
{
name: "keyword-container",
text: `a.near.b
`,
expErr: `d2/testdata/d2compiler/TestCompile/keyword-container.d2:1:3: "near" must be the last part of the key`,
},
{
name: "escaped_id",
@ -2377,11 +2408,17 @@ d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are
a
b
c
d.invalid descendant
d.valid descendant
e: {
grid-rows: 1
grid-columns: 2
a
b
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_nested.d2:2:2: "grid-rows" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")
d2/testdata/d2compiler/TestCompile/grid_nested.d2:3:2: "grid-columns" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")`,
expErr: ``,
},
{
name: "classes",
@ -2460,6 +2497,29 @@ classes.x.shape: diamond
tassert.Equal(t, "diamond", g.Objects[0].Shape.Value)
},
},
{
name: "nested-array-classes",
text: `classes: {
one target: {
target-arrowhead.label: 1
}
association: {
target-arrowhead.shape: arrow
}
}
a -> b: { class: [one target; association] }
a -> b: { class: [association; one target] }
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
// They have the same, regardless of order of class application
// since the classes modify attributes exclusive of each other
tassert.Equal(t, "1", g.Edges[0].DstArrowhead.Label.Value)
tassert.Equal(t, "1", g.Edges[1].DstArrowhead.Label.Value)
tassert.Equal(t, "arrow", g.Edges[0].DstArrowhead.Shape.Value)
tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
},
},
{
name: "class-shape-class",
text: `classes: {

View file

@ -11,6 +11,7 @@ import (
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
)
func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
@ -295,9 +296,16 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
connection.LabelPosition = *edge.LabelPosition
}
if edge.LabelPercentage != nil {
connection.LabelPercentage = *edge.LabelPercentage
connection.LabelPercentage = float64(float32(*edge.LabelPercentage))
}
connection.Route = edge.Route
connection.Route = make([]*geo.Point, 0, len(edge.Route))
for i := range edge.Route {
p := edge.Route[i].Copy()
p.TruncateDecimals()
p.TruncateFloat32()
connection.Route = append(connection.Route, p)
}
connection.IsCurve = edge.IsCurve
connection.Src = edge.Src.AbsID()

View file

@ -1,6 +1,7 @@
package d2graph
import (
"bytes"
"context"
"errors"
"fmt"
@ -1054,6 +1055,45 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R
return &dims, nil
}
// 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))
var fitWidth, fitHeight float64
if shapeType == shape.PERSON_TYPE {
fitWidth = contentWidth + paddingX
fitHeight = contentHeight + paddingY
} else {
fitWidth, fitHeight = s.GetDimensionsToFit(contentWidth, contentHeight, paddingX, paddingY)
}
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
obj.Height = sideLength
} else if desiredHeight == 0 || desiredWidth == 0 {
switch s.GetType() {
case shape.PERSON_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT)
case shape.OVAL_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.OVAL_AR_LIMIT)
}
}
}
func (obj *Object) OuterNearContainer() *Object {
for obj != nil {
if obj.NearKey != nil {
@ -1437,7 +1477,6 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(defaultDims.Width), float64(defaultDims.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
s := shape.NewShape(shapeType, contentBox)
paddingX, paddingY := s.GetDefaultPadding()
if desiredWidth != 0 {
paddingX = 0.
@ -1470,27 +1509,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
var fitWidth, fitHeight float64
if shapeType == shape.PERSON_TYPE {
fitWidth = contentBox.Width + paddingX
fitHeight = contentBox.Height + paddingY
} else {
fitWidth, fitHeight = s.GetDimensionsToFit(contentBox.Width, contentBox.Height, paddingX, paddingY)
}
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
obj.Height = sideLength
} else if desiredHeight == 0 || desiredWidth == 0 {
switch s.GetType() {
case shape.PERSON_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT)
case shape.OVAL_TYPE:
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.OVAL_AR_LIMIT)
}
}
obj.SizeToContent(contentBox.Width, contentBox.Height, paddingX, paddingY)
}
for _, edge := range g.Edges {
usedFont := fontFamily
@ -1601,9 +1620,6 @@ func Key(k *d2ast.KeyPath) []string {
// All reserved keywords. See init below.
var ReservedKeywords map[string]struct{}
// All reserved keywords not including style keywords.
var ReservedKeywords2 map[string]struct{}
// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
@ -1625,16 +1641,20 @@ var SimpleReservedKeywords = map[string]struct{}{
"vertical-gap": {},
"horizontal-gap": {},
"class": {},
"classes": {},
}
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
var ReservedKeywordHolders = map[string]struct{}{
"style": {},
"source-arrowhead": {},
"target-arrowhead": {},
}
// CompositeReservedKeywords are reserved keywords that can hold composites
var CompositeReservedKeywords = map[string]struct{}{
"classes": {},
}
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
var StyleKeywords = map[string]struct{}{
"opacity": {},
@ -1708,23 +1728,15 @@ func init() {
ReservedKeywords[k] = v
}
for k, v := range ReservedKeywordHolders {
ReservedKeywords[k] = v
CompositeReservedKeywords[k] = v
}
for k, v := range BoardKeywords {
CompositeReservedKeywords[k] = v
}
for k, v := range CompositeReservedKeywords {
ReservedKeywords[k] = v
}
ReservedKeywords2 = make(map[string]struct{})
for k, v := range SimpleReservedKeywords {
ReservedKeywords2[k] = v
}
for k, v := range ReservedKeywordHolders {
ReservedKeywords2[k] = v
}
for k, v := range BoardKeywords {
ReservedKeywords2[k] = v
}
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
for _, k := range NearConstantsArray {
NearConstants[k] = struct{}{}
@ -1799,3 +1811,28 @@ func (g *Graph) ApplyTheme(themeID int64) error {
g.Theme = &theme
return nil
}
func (g *Graph) PrintString() string {
buf := &bytes.Buffer{}
fmt.Fprint(buf, "Objects: [")
for _, obj := range g.Objects {
fmt.Fprintf(buf, "%#v @(%v)", obj.AbsID(), obj.TopLeft.ToString())
}
fmt.Fprint(buf, "]")
return buf.String()
}
func (obj *Object) IterDescendants(apply func(parent, child *Object)) {
for _, c := range obj.ChildrenArray {
apply(obj, c)
c.IterDescendants(apply)
}
}
func (obj *Object) IsMultiple() bool {
return obj.Style.Multiple != nil && obj.Style.Multiple.Value == "true"
}
func (obj *Object) Is3D() bool {
return obj.Style.ThreeDee != nil && obj.Style.ThreeDee.Value == "true"
}

315
d2graph/layout.go Normal file
View file

@ -0,0 +1,315 @@
package d2graph
import (
"strings"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)
func (obj *Object) MoveWithDescendants(dx, dy float64) {
obj.TopLeft.X += dx
obj.TopLeft.Y += dy
for _, child := range obj.ChildrenArray {
child.MoveWithDescendants(dx, dy)
}
}
func (obj *Object) MoveWithDescendantsTo(x, y float64) {
dx := x - obj.TopLeft.X
dy := y - obj.TopLeft.Y
obj.MoveWithDescendants(dx, dy)
}
func (parent *Object) removeChild(child *Object) {
delete(parent.Children, strings.ToLower(child.ID))
for i := 0; i < len(parent.ChildrenArray); i++ {
if parent.ChildrenArray[i] == child {
parent.ChildrenArray = append(parent.ChildrenArray[:i], parent.ChildrenArray[i+1:]...)
break
}
}
}
// remove obj and all descendants from graph, as a new Graph
func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph {
descendantObjects, edges := pluckObjAndEdges(g, obj)
tempGraph := NewGraph()
tempGraph.Root.ChildrenArray = []*Object{obj}
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
for _, descendantObj := range descendantObjects {
descendantObj.Graph = tempGraph
}
tempGraph.Objects = descendantObjects
tempGraph.Edges = edges
obj.Parent.removeChild(obj)
obj.Parent = tempGraph.Root
return tempGraph
}
func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) {
for i := 0; i < len(g.Edges); i++ {
edge := g.Edges[i]
if edge.Src == obj || edge.Dst == obj {
edges = append(edges, edge)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i--
}
}
for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() {
descendantsObjects = append(descendantsObjects, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray {
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
}
}
return descendantsObjects, edges
}
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
obj := tempGraph.Root.ChildrenArray[0]
obj.MoveWithDescendantsTo(0, 0)
obj.Parent = parent
for _, obj := range tempGraph.Objects {
obj.Graph = g
}
g.Objects = append(g.Objects, tempGraph.Objects...)
parent.Children[strings.ToLower(obj.ID)] = obj
parent.ChildrenArray = append(parent.ChildrenArray, obj)
g.Edges = append(g.Edges, tempGraph.Edges...)
}
// ShiftDescendants moves Object's descendants (not including itself)
// descendants' edges are also moved by the same dx and dy (the whole route is moved if both ends are a descendant)
func (obj *Object) ShiftDescendants(dx, dy float64) {
// also need to shift edges of descendants that are shifted
movedEdges := make(map[*Edge]struct{})
for _, e := range obj.Graph.Edges {
isSrcDesc := e.Src.IsDescendantOf(obj)
isDstDesc := e.Dst.IsDescendantOf(obj)
if isSrcDesc && isDstDesc {
movedEdges[e] = struct{}{}
for _, p := range e.Route {
p.X += dx
p.Y += dy
}
}
}
obj.IterDescendants(func(_, curr *Object) {
curr.TopLeft.X += dx
curr.TopLeft.Y += dy
for _, e := range obj.Graph.Edges {
if _, ok := movedEdges[e]; ok {
continue
}
isSrc := e.Src == curr
isDst := e.Dst == curr
if isSrc && isDst {
for _, p := range e.Route {
p.X += dx
p.Y += dy
}
} else if isSrc {
if dx == 0 {
e.ShiftStart(dy, false)
} else if dy == 0 {
e.ShiftStart(dx, true)
} else {
e.Route[0].X += dx
e.Route[0].Y += dy
}
} else if isDst {
if dx == 0 {
e.ShiftEnd(dy, false)
} else if dy == 0 {
e.ShiftEnd(dx, true)
} else {
e.Route[len(e.Route)-1].X += dx
e.Route[len(e.Route)-1].Y += dy
}
}
if isSrc || isDst {
movedEdges[e] = struct{}{}
}
}
})
}
// ShiftStart moves the starting point of the route by delta either horizontally or vertically
// if subsequent points are in line with the movement, they will be removed (unless it is the last point)
// start end
// . ├────┼────┼───┼────┼───┤ before
// . ├──dx──►
// . ├──┼───┼────┼───┤ after
func (edge *Edge) ShiftStart(delta float64, isHorizontal bool) {
position := func(p *geo.Point) float64 {
if isHorizontal {
return p.X
}
return p.Y
}
start := edge.Route[0]
next := edge.Route[1]
isIncreasing := position(start) < position(next)
if isHorizontal {
start.X += delta
} else {
start.Y += delta
}
if isIncreasing == (delta < 0) {
// nothing more to do when moving away from the next point
return
}
isAligned := func(p *geo.Point) bool {
if isHorizontal {
return p.Y == start.Y
}
return p.X == start.X
}
isPastStart := func(p *geo.Point) bool {
if delta > 0 {
return position(p) < position(start)
} else {
return position(p) > position(start)
}
}
needsRemoval := false
toRemove := make([]bool, len(edge.Route))
for i := 1; i < len(edge.Route)-1; i++ {
if !isAligned(edge.Route[i]) {
break
}
if isPastStart(edge.Route[i]) {
toRemove[i] = true
needsRemoval = true
}
}
if needsRemoval {
edge.Route = geo.RemovePoints(edge.Route, toRemove)
}
}
// ShiftEnd moves the ending point of the route by delta either horizontally or vertically
// if prior points are in line with the movement, they will be removed (unless it is the first point)
func (edge *Edge) ShiftEnd(delta float64, isHorizontal bool) {
position := func(p *geo.Point) float64 {
if isHorizontal {
return p.X
}
return p.Y
}
end := edge.Route[len(edge.Route)-1]
prev := edge.Route[len(edge.Route)-2]
isIncreasing := position(prev) < position(end)
if isHorizontal {
end.X += delta
} else {
end.Y += delta
}
if isIncreasing == (delta > 0) {
// nothing more to do when moving away from the next point
return
}
isAligned := func(p *geo.Point) bool {
if isHorizontal {
return p.Y == end.Y
}
return p.X == end.X
}
isPastEnd := func(p *geo.Point) bool {
if delta > 0 {
return position(p) < position(end)
} else {
return position(p) > position(end)
}
}
needsRemoval := false
toRemove := make([]bool, len(edge.Route))
for i := len(edge.Route) - 2; i > 0; i-- {
if !isAligned(edge.Route[i]) {
break
}
if isPastEnd(edge.Route[i]) {
toRemove[i] = true
needsRemoval = true
}
}
if needsRemoval {
edge.Route = geo.RemovePoints(edge.Route, toRemove)
}
}
// GetModifierElementAdjustments returns width/height adjustments to account for shapes with 3d or multiple
func (obj *Object) GetModifierElementAdjustments() (dx, dy float64) {
if obj.Is3D() {
if obj.Shape.Value == d2target.ShapeHexagon {
dy = d2target.THREE_DEE_OFFSET / 2
} else {
dy = d2target.THREE_DEE_OFFSET
}
dx = d2target.THREE_DEE_OFFSET
} else if obj.IsMultiple() {
dy = d2target.MULTIPLE_OFFSET
dx = d2target.MULTIPLE_OFFSET
}
return dx, dy
}
func (obj *Object) ToShape() shape.Shape {
tl := obj.TopLeft
if tl == nil {
tl = geo.NewPoint(0, 0)
}
dslShape := strings.ToLower(obj.Shape.Value)
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
contentBox := geo.NewBox(tl, obj.Width, obj.Height)
return shape.NewShape(shapeType, contentBox)
}
func (obj *Object) GetLabelTopLeft() *geo.Point {
if obj.LabelPosition == nil {
return nil
}
s := obj.ToShape()
labelPosition := label.Position(*obj.LabelPosition)
var box *geo.Box
if labelPosition.IsOutside() {
box = s.GetBox()
} else {
box = s.GetInnerBox()
}
labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
float64(obj.LabelDimensions.Width),
float64(obj.LabelDimensions.Height),
)
return labelTL
}

View file

@ -669,10 +669,9 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext) (*Field,
if _, ok := d2graph.ReservedKeywords[strings.ToLower(head)]; ok {
head = strings.ToLower(head)
}
if head == "class" && i < len(kp.Path)-1 {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), `"class" must be the last part of the key`)
if _, ok := d2graph.CompositeReservedKeywords[head]; !ok && i < len(kp.Path)-1 {
return nil, d2parser.Errorf(kp.Path[i].Unbox(), fmt.Sprintf(`"%s" must be the last part of the key`, head))
}
}
if head == "_" {

View file

@ -119,9 +119,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
s := obj.ToShape()
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
// Since dagre container labels are pushed up, we don't want a child container to collide
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, (iconSize+label.PADDING*2)*2)
@ -159,7 +157,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
id := obj.AbsID()
idToObj[id] = obj
height := obj.Height
width, height := obj.Width, obj.Height
if obj.HasLabel() {
if obj.HasOutsideBottomLabel() || obj.Icon != nil {
height += float64(obj.LabelDimensions.Height) + label.PADDING
@ -168,7 +166,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
height += float64(obj.LabelDimensions.Height) + label.PADDING
}
}
loadScript += generateAddNodeLine(id, int(obj.Width), int(height))
// reserve extra space for 3d/multiple by providing dagre the larger dimensions
dx, dy := obj.GetModifierElementAdjustments()
width += dx
height += dy
loadScript += generateAddNodeLine(id, int(width), int(height))
if obj.Parent != g.Root {
loadScript += generateAddParentLine(id, obj.Parent.AbsID())
}
@ -232,10 +235,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// dagre gives center of node
obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
obj.Width = dn.Width
obj.Height = dn.Height
obj.Width = math.Ceil(dn.Width)
obj.Height = math.Ceil(dn.Height)
if obj.HasLabel() {
if obj.HasLabel() && obj.LabelPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else if obj.HasOutsideBottomLabel() {
@ -248,7 +251,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.Icon != nil {
if obj.Icon != nil && obj.IconPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
@ -381,7 +384,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if isHorizontal && e.Src.Parent != g.Root && e.Dst.Parent != g.Root {
moveWholeEdge = true
} else {
e.Route[0].Y += stepSize
e.ShiftStart(stepSize, false)
}
}
}
@ -390,7 +393,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if isHorizontal && e.Dst.Parent != g.Root && e.Src.Parent != g.Root {
moveWholeEdge = true
} else {
e.Route[len(e.Route)-1].Y += stepSize
e.ShiftEnd(stepSize, false)
}
}
}
@ -407,6 +410,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
// remove the extra width/height we added for 3d/multiple after all objects/connections are placed
// and shift the shapes down accordingly
for _, obj := range g.Objects {
dx, dy := obj.GetModifierElementAdjustments()
if dx != 0 || dy != 0 {
obj.TopLeft.Y += dy
obj.ShiftDescendants(0, dy)
if !obj.IsContainer() {
obj.Width -= dx
obj.Height -= dy
}
}
}
for _, edge := range g.Edges {
points := edge.Route
startIndex, endIndex := 0, len(points)-1
@ -453,8 +470,27 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
var originalSrcTL, originalDstTL *geo.Point
// if the edge passes through 3d/multiple, use the offset box for tracing to border
if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
if start.X > edge.Src.TopLeft.X+srcDx &&
start.Y < edge.Src.TopLeft.Y+edge.Src.Height-srcDy {
originalSrcTL = edge.Src.TopLeft.Copy()
edge.Src.TopLeft.X += srcDx
edge.Src.TopLeft.Y -= srcDy
}
}
if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
if end.X > edge.Dst.TopLeft.X+dstDx &&
end.Y < edge.Dst.TopLeft.Y+edge.Dst.Height-dstDy {
originalDstTL = edge.Dst.TopLeft.Copy()
edge.Dst.TopLeft.X += dstDx
edge.Dst.TopLeft.Y -= dstDy
}
}
srcShape := edge.Src.ToShape()
dstShape := edge.Dst.ToShape()
// trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1])
@ -517,6 +553,16 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
if edge.Label.Value != "" {
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
// undo 3d/multiple offset
if originalSrcTL != nil {
edge.Src.TopLeft.X = originalSrcTL.X
edge.Src.TopLeft.Y = originalSrcTL.Y
}
if originalDstTL != nil {
edge.Dst.TopLeft.X = originalDstTL.X
edge.Dst.TopLeft.Y = originalDstTL.Y
}
}
return nil

View file

@ -222,6 +222,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
width = go2.Max(width, float64(obj.LabelDimensions.Width))
}
// reserve extra space for 3d/multiple by providing elk the larger dimensions
dx, dy := obj.GetModifierElementAdjustments()
width += dx
height += dy
n := &ELKNode{
ID: obj.AbsID(),
@ -400,10 +404,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
parentY = parent.TopLeft.Y
}
obj.TopLeft = geo.NewPoint(parentX+n.X, parentY+n.Y)
obj.Width = n.Width
obj.Height = n.Height
obj.Width = math.Ceil(n.Width)
obj.Height = math.Ceil(n.Height)
if obj.HasLabel() {
if obj.HasLabel() && obj.LabelPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
} else if obj.HasOutsideBottomLabel() {
@ -415,7 +419,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.Icon != nil {
if obj.Icon != nil && obj.IconPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
@ -454,10 +458,51 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Y: parentY + s.End.Y,
})
}
edge.Route = points
}
// remove the extra width/height we added for 3d/multiple after all objects/connections are placed
// and shift the shapes down accordingly
for _, obj := range g.Objects {
dx, dy := obj.GetModifierElementAdjustments()
if dx != 0 || dy != 0 {
obj.TopLeft.Y += dy
obj.ShiftDescendants(0, dy)
if !obj.IsContainer() {
obj.Width -= dx
obj.Height -= dy
}
}
}
for _, edge := range g.Edges {
points := edge.Route
startIndex, endIndex := 0, len(points)-1
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
start := points[startIndex]
end := points[endIndex]
var originalSrcTL, originalDstTL *geo.Point
// if the edge passes through 3d/multiple, use the offset box for tracing to border
if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
if start.X > edge.Src.TopLeft.X+srcDx &&
start.Y < edge.Src.TopLeft.Y+edge.Src.Height-srcDy {
originalSrcTL = edge.Src.TopLeft.Copy()
edge.Src.TopLeft.X += srcDx
edge.Src.TopLeft.Y -= srcDy
}
}
if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
if end.X > edge.Dst.TopLeft.X+dstDx &&
end.Y < edge.Dst.TopLeft.Y+edge.Dst.Height-dstDy {
originalDstTL = edge.Dst.TopLeft.Copy()
edge.Dst.TopLeft.X += dstDx
edge.Dst.TopLeft.Y -= dstDy
}
}
srcShape := edge.Src.ToShape()
dstShape := edge.Dst.ToShape()
// trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1])
@ -468,6 +513,16 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
edge.Route = points
// undo 3d/multiple offset
if originalSrcTL != nil {
edge.Src.TopLeft.X = originalSrcTL.X
edge.Src.TopLeft.Y = originalSrcTL.Y
}
if originalDstTL != nil {
edge.Dst.TopLeft.X = originalDstTL.X
edge.Dst.TopLeft.Y = originalDstTL.Y
}
}
deleteBends(g)
@ -506,10 +561,11 @@ func deleteBends(g *d2graph.Graph) {
}
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
dx, dy := endpoint.GetModifierElementAdjustments()
// Make sure it's still attached
if isHorizontal {
if end.Y <= endpoint.TopLeft.Y+10 {
if end.Y <= endpoint.TopLeft.Y+10-dy {
continue
}
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
@ -519,7 +575,7 @@ func deleteBends(g *d2graph.Graph) {
if end.X <= endpoint.TopLeft.X+10 {
continue
}
if end.X >= endpoint.TopLeft.X+endpoint.Width-10 {
if end.X >= endpoint.TopLeft.X+endpoint.Width-10+dx {
continue
}
}

View file

@ -0,0 +1,13 @@
package d2grid
const (
// don't consider layouts with rows longer than targetSize*1.2 or shorter than targetSize/1.2
STARTING_THRESHOLD = 1.2
// next try layouts with a 25% larger threshold
THRESHOLD_STEP_SIZE = 0.25
MIN_THRESHOLD_ATTEMPTS = 1
MAX_THRESHOLD_ATTEMPTS = 3
ATTEMPT_LIMIT = 100_000
SKIP_LIMIT = 10_000_000
)

View file

@ -5,6 +5,7 @@ import (
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
)
type gridDiagram struct {
@ -95,22 +96,30 @@ func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd.horizontalGap, _ = strconv.Atoi(root.HorizontalGap.Value)
}
for _, o := range gd.objects {
o.TopLeft = geo.NewPoint(0, 0)
}
return &gd
}
func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects {
obj.TopLeft.X += dx
obj.TopLeft.Y += dy
obj.MoveWithDescendants(dx, dy)
}
}
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range gd.objects {
obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child)
restore := func(parent, child *d2graph.Object) {
parent.Children[strings.ToLower(child.ID)] = child
parent.ChildrenArray = append(parent.ChildrenArray, child)
graph.Objects = append(graph.Objects, child)
}
for _, child := range gd.objects {
restore(obj, child)
child.IterDescendants(restore)
}
graph.Objects = append(graph.Objects, gd.objects...)
}

View file

@ -1,12 +1,14 @@
package d2grid
import (
"bytes"
"context"
"fmt"
"math"
"sort"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
@ -29,7 +31,7 @@ const (
// 7. Put grid children back in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
return func(ctx context.Context, g *d2graph.Graph) error {
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g)
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g, layout)
if err != nil {
return err
}
@ -45,10 +47,162 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d
}
}
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
toRemove := make(map[*d2graph.Object]struct{})
gridDiagrams = make(map[string]*gridDiagram)
objectOrder = make(map[string]int)
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
}
var processGrid func(obj *d2graph.Object) error
processGrid = func(obj *d2graph.Object) error {
for _, child := range obj.ChildrenArray {
if child.IsGridDiagram() {
if err := processGrid(child); err != nil {
return err
}
} else if len(child.ChildrenArray) > 0 {
tempGraph := g.ExtractAsNestedGraph(child)
if err := layout(ctx, tempGraph); err != nil {
return err
}
g.InjectNestedGraph(tempGraph, obj)
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
})
sort.SliceStable(child.ChildrenArray, func(i, j int) bool {
return objectOrder[child.ChildrenArray[i].AbsID()] < objectOrder[child.ChildrenArray[j].AbsID()]
})
sort.SliceStable(obj.ChildrenArray, func(i, j int) bool {
return objectOrder[obj.ChildrenArray[i].AbsID()] < objectOrder[obj.ChildrenArray[j].AbsID()]
})
for _, o := range tempGraph.Objects {
toRemove[o] = struct{}{}
}
}
}
gd, err := layoutGrid(g, obj)
if err != nil {
return err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
if obj.Box != nil {
// CONTAINER_PADDING is default, but use gap value if set
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
if obj.GridGap != nil || obj.HorizontalGap != nil {
horizontalPadding = gd.horizontalGap
}
if obj.GridGap != nil || obj.VerticalGap != nil {
verticalPadding = gd.verticalGap
}
// size shape according to grid
obj.SizeToContent(gd.width, gd.height, float64(2*horizontalPadding), float64(2*verticalPadding))
// compute where the grid should be placed inside shape
s := obj.ToShape()
innerBox := s.GetInnerBox()
if innerBox.TopLeft.X != 0 || innerBox.TopLeft.Y != 0 {
gd.shift(innerBox.TopLeft.X, innerBox.TopLeft.Y)
}
// compute how much space the label and icon occupy
var occupiedWidth, occupiedHeight float64
if obj.Icon != nil {
iconSpace := float64(d2target.MAX_ICON_SIZE + 2*label.PADDING)
occupiedWidth = iconSpace
occupiedHeight = iconSpace
}
var dx, dy float64
if obj.LabelDimensions.Height != 0 {
occupiedHeight = math.Max(
occupiedHeight,
float64(obj.LabelDimensions.Height)+2*label.PADDING,
)
}
if obj.LabelDimensions.Width != 0 {
// . ├────┤───────├────┤
// . icon label icon
// with an icon in top left we need 2x the space to fit the label in the center
occupiedWidth *= 2
occupiedWidth += float64(obj.LabelDimensions.Width) + 2*label.PADDING
if occupiedWidth > obj.Width {
dx = (occupiedWidth - obj.Width) / 2
obj.Width = occupiedWidth
}
}
// also check for grid cells with outside top labels or icons
// the first grid object is at the top (and always exists)
topY := gd.objects[0].TopLeft.Y
highestOutside := topY
for _, o := range gd.objects {
// we only want to compute label positions for objects at the top of the grid
if o.TopLeft.Y > topY {
if gd.rowDirected {
// if the grid is rowDirected (row1, row2, etc) we can stop after finishing the first row
break
} else {
// otherwise we continue until the next column
continue
}
}
if o.LabelPosition != nil {
labelPosition := label.Position(*o.LabelPosition)
if labelPosition.IsOutside() {
labelTL := o.GetLabelTopLeft()
if labelTL.Y < highestOutside {
highestOutside = labelTL.Y
}
}
}
if o.IconPosition != nil {
switch label.Position(*o.IconPosition) {
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
iconSpace := float64(d2target.MAX_ICON_SIZE + label.PADDING)
if topY-iconSpace < highestOutside {
highestOutside = topY - iconSpace
}
}
}
}
if highestOutside < topY {
occupiedHeight += topY - highestOutside + 2*label.PADDING
}
if occupiedHeight > float64(verticalPadding) {
// if the label doesn't fit within the padding, we need to add more
dy = occupiedHeight - float64(verticalPadding)
obj.Height += dy
}
// we need to center children if we have to expand to fit the container label
if dx != 0 || dy != 0 {
gd.shift(dx, dy)
}
}
if obj.HasLabel() {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
}
if obj.Icon != nil {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
}
gridDiagrams[obj.AbsID()] = gd
for _, o := range gd.objects {
toRemove[o] = struct{}{}
}
return nil
}
if len(g.Objects) > 0 {
queue := make([]*d2graph.Object, 1, len(g.Objects))
queue[0] = g.Root
@ -63,47 +217,14 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams ma
continue
}
gd, err := layoutGrid(g, obj)
if err != nil {
if err := processGrid(obj); err != nil {
return nil, nil, err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
var dx, dy float64
width := gd.width + 2*CONTAINER_PADDING
labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
if labelWidth > width {
dx = (labelWidth - width) / 2
width = labelWidth
}
height := gd.height + 2*CONTAINER_PADDING
labelHeight := float64(obj.LabelDimensions.Height) + 2*label.PADDING
if labelHeight > CONTAINER_PADDING {
// if the label doesn't fit within the padding, we need to add more
grow := labelHeight - CONTAINER_PADDING
dy = grow / 2
height += grow
}
// we need to center children if we have to expand to fit the container label
if dx != 0 || dy != 0 {
gd.shift(dx, dy)
}
obj.Box = geo.NewBox(nil, width, height)
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
gridDiagrams[obj.AbsID()] = gd
for _, o := range gd.objects {
toRemove[o] = struct{}{}
}
}
}
objectOrder = make(map[string]int)
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
for _, obj := range g.Objects {
if _, exists := toRemove[obj]; !exists {
layoutObjects = append(layoutObjects, obj)
}
@ -125,10 +246,17 @@ func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
// position labels and icons
for _, o := range gd.objects {
if o.Icon != nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
// don't overwrite position if nested graph layout positioned label/icon
if o.LabelPosition == nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
}
if o.IconPosition == nil {
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
} else {
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
if o.LabelPosition == nil {
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
}
@ -191,7 +319,7 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
}
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
cursor.X += o.Width + horizontalGap
}
cursor.X = 0
@ -206,7 +334,7 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
}
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
cursor.Y += o.Height + verticalGap
}
cursor.X += colWidths[j] + horizontalGap
@ -279,6 +407,8 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
maxX = math.Max(maxX, rowWidth)
}
// TODO if object is a nested grid, consider growing descendants according to the inner grid layout
// then expand thinnest objects to make each row the same width
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . │ │ │ │ │ │ │
@ -342,7 +472,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
for _, row := range layout {
rowHeight := 0.
for _, o := range row {
o.TopLeft = cursor.Copy()
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
cursor.X += o.Width + horizontalGap
rowHeight = math.Max(rowHeight, o.Height)
}
@ -430,7 +560,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
for _, column := range layout {
colWidth := 0.
for _, o := range column {
o.TopLeft = cursor.Copy()
o.MoveWithDescendantsTo(cursor.X, cursor.Y)
cursor.Y += o.Height + verticalGap
colWidth = math.Max(colWidth, o.Width)
}
@ -452,6 +582,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// generate the best layout of objects aiming for each row to be the targetSize width
// if columns is true, each column aims to have the targetSize height
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
debug := false
var nCuts int
if columns {
nCuts = gd.columns - 1
@ -462,6 +593,23 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
return genLayout(gd.objects, nil)
}
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
fastIsBest := false
// try fast layout algorithm as a baseline
if fastLayout := gd.fastLayout(targetSize, nCuts, columns); fastLayout != nil {
dist := getDistToTarget(fastLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if debug {
fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1))
}
if dist == 0 {
return fastLayout
}
bestDist = dist
bestLayout = fastLayout
fastIsBest = true
}
var gap float64
if columns {
gap = float64(gd.verticalGap)
@ -476,17 +624,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
}
}
debug := false
sizes := []float64{}
for _, obj := range gd.objects {
size := getSize(obj)
sizes = append(sizes, size)
}
sd := stddev(sizes)
if debug {
fmt.Printf("sizes (%d): %v\n", len(sizes), sizes)
fmt.Printf("std dev: %v; targetSize %v\n", sd, targetSize)
}
skipCount := 0
count := 0
// quickly eliminate bad row groupings
startingCache := make(map[int]bool)
// try to find a layout with all rows within 1.2*targetSize
// skip options with a row that is 1.2*longer or shorter
// Note: we want a low threshold to explore good options within attemptLimit,
// but the best option may require a few rows that are far from the target size.
okThreshold := 1.2
// if we don't find a layout try 25% larger threshold
thresholdStep := 0.25
okThreshold := STARTING_THRESHOLD
rowOk := func(row []*d2graph.Object, starting bool) (ok bool) {
if starting {
// we can cache results from starting positions since they repeat and don't change
@ -509,21 +664,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
// if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
if rowSize > okThreshold*targetSize {
skipCount++
if skipCount >= SKIP_LIMIT {
// there may even be too many to skip
return true
}
return false
}
}
// row is too small to be good overall
if rowSize < targetSize/okThreshold {
skipCount++
if skipCount >= SKIP_LIMIT {
return true
}
return false
}
return true
}
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
count := 0
attemptLimit := 100_000
// get all options for where to place these cuts, preferring later cuts over earlier cuts
// with 5 objects and 2 cuts we have these options:
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
@ -539,33 +697,94 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
if dist < bestDist {
bestLayout = layout
bestDist = dist
fastIsBest = false
} else if fastIsBest && dist == bestDist {
// prefer ordered search solution to fast layout solution
bestLayout = layout
fastIsBest = false
}
count++
// with few objects we can try all options to get best result but this won't scale, so only try up to 100k options
return count >= attemptLimit
return count >= ATTEMPT_LIMIT || skipCount >= SKIP_LIMIT
}
// try at least 3 different okThresholds
for i := 0; i < 3 || bestLayout == nil; i++ {
// try number of different okThresholds depending on std deviation of sizes
thresholdAttempts := int(math.Ceil(sd))
if thresholdAttempts < MIN_THRESHOLD_ATTEMPTS {
thresholdAttempts = MIN_THRESHOLD_ATTEMPTS
} else if thresholdAttempts > MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts = MAX_THRESHOLD_ATTEMPTS
}
for i := 0; i < thresholdAttempts || bestLayout == nil; i++ {
count = 0.
skipCount = 0.
iterDivisions(gd.objects, nCuts, tryDivision, rowOk)
okThreshold += thresholdStep
okThreshold += THRESHOLD_STEP_SIZE
if debug {
fmt.Printf("increasing ok threshold to %v\n", okThreshold)
fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold)
}
startingCache = make(map[int]bool)
count = 0.
}
if debug {
fmt.Printf("final count %d, skip count %d\n", count, skipCount)
if skipCount == 0 {
// threshold isn't skipping anything so increasing it won't help
break
}
// okThreshold isn't high enough yet, we skipped every option so don't count it
if count == 0 && thresholdAttempts < MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts++
}
}
if debug {
fmt.Printf("best layout: %v\n", layoutString(bestLayout, sizes))
}
return bestLayout
}
func sum(values []float64) float64 {
s := 0.
for _, v := range values {
s += v
}
return s
}
func avg(values []float64) float64 {
return sum(values) / float64(len(values))
}
func variance(values []float64) float64 {
mean := avg(values)
total := 0.
for _, value := range values {
dev := mean - value
total += dev * dev
}
return total / float64(len(values))
}
func stddev(values []float64) float64 {
return math.Sqrt(variance(values))
}
func (gd *gridDiagram) fastLayout(targetSize float64, nCuts int, columns bool) (layout [][]*d2graph.Object) {
var gap float64
if columns {
gap = float64(gd.verticalGap)
} else {
gap = float64(gd.horizontalGap)
}
// try fast layout algorithm, see if it is better than first 1mil attempts
debt := 0.
fastDivision := make([]int, 0, nCuts)
rowSize := 0.
for i := 0; i < len(gd.objects); i++ {
o := gd.objects[i]
size := getSize(o)
var size float64
if columns {
size = o.Height
} else {
size = o.Width
}
if rowSize == 0 {
if size > targetSize-debt {
fastDivision = append(fastDivision, i-1)
@ -588,14 +807,23 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
}
}
if len(fastDivision) == nCuts {
layout := genLayout(gd.objects, fastDivision)
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if dist < bestDist {
bestLayout = layout
bestDist = dist
}
layout = genLayout(gd.objects, fastDivision)
}
return bestLayout
return layout
}
func layoutString(layout [][]*d2graph.Object, sizes []float64) string {
buf := &bytes.Buffer{}
i := 0
fmt.Fprintf(buf, "[\n")
for _, r := range layout {
vals := sizes[i : i+len(r)]
fmt.Fprintf(buf, "%v:\t%v\n", sum(vals), vals)
i += len(r)
}
fmt.Fprintf(buf, "]\n")
return buf.String()
}
// process current division, return true to stop iterating
@ -704,25 +932,42 @@ func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objects
})
}()
var restore func(obj *d2graph.Object)
restore = func(obj *d2graph.Object) {
gd, exists := gridDiagrams[obj.AbsID()]
if !exists {
return
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
horizontalPadding, verticalPadding := CONTAINER_PADDING, CONTAINER_PADDING
if obj.GridGap != nil || obj.HorizontalGap != nil {
horizontalPadding = gd.horizontalGap
}
if obj.GridGap != nil || obj.VerticalGap != nil {
verticalPadding = gd.verticalGap
}
// shift the grid from (0, 0)
gd.shift(
obj.TopLeft.X+float64(horizontalPadding),
obj.TopLeft.Y+float64(verticalPadding),
)
gd.cleanup(obj, graph)
for _, child := range obj.ChildrenArray {
restore(child)
}
}
if graph.Root.IsGridDiagram() {
gd, exists := gridDiagrams[graph.Root.AbsID()]
if exists {
gd.cleanup(graph.Root, graph)
return
}
}
for _, obj := range graph.Objects {
gd, exists := gridDiagrams[obj.AbsID()]
if !exists {
continue
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
// shift the grid from (0, 0)
gd.shift(
obj.TopLeft.X+CONTAINER_PADDING,
obj.TopLeft.Y+CONTAINER_PADDING,
)
gd.cleanup(obj, graph)
restore(obj)
}
}

View file

@ -148,62 +148,14 @@ func WithoutConstantNears(ctx context.Context, g *d2graph.Graph) (constantNearGr
}
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isConst {
descendantObjects, edges := pluckObjAndEdges(g, obj)
tempGraph := d2graph.NewGraph()
tempGraph.Root.ChildrenArray = []*d2graph.Object{obj}
tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj
for _, descendantObj := range descendantObjects {
descendantObj.Graph = tempGraph
}
tempGraph.Objects = descendantObjects
tempGraph.Edges = edges
tempGraph := g.ExtractAsNestedGraph(obj)
constantNearGraphs = append(constantNearGraphs, tempGraph)
i--
delete(obj.Parent.Children, strings.ToLower(obj.ID))
for i := 0; i < len(obj.Parent.ChildrenArray); i++ {
if obj.Parent.ChildrenArray[i] == obj {
obj.Parent.ChildrenArray = append(obj.Parent.ChildrenArray[:i], obj.Parent.ChildrenArray[i+1:]...)
break
}
}
obj.Parent = tempGraph.Root
}
}
return constantNearGraphs
}
func pluckObjAndEdges(g *d2graph.Graph, obj *d2graph.Object) (descendantsObjects []*d2graph.Object, edges []*d2graph.Edge) {
for i := 0; i < len(g.Edges); i++ {
edge := g.Edges[i]
if edge.Src == obj || edge.Dst == obj {
edges = append(edges, edge)
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
i--
}
}
for i := 0; i < len(g.Objects); i++ {
temp := g.Objects[i]
if temp.AbsID() == obj.AbsID() {
descendantsObjects = append(descendantsObjects, obj)
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
for _, child := range obj.ChildrenArray {
subObjects, subEdges := pluckObjAndEdges(g, child)
descendantsObjects = append(descendantsObjects, subObjects...)
edges = append(edges, subEdges...)
}
break
}
}
return descendantsObjects, edges
}
// boundingBox gets the center of the graph as defined by shapes
// The bounds taking into consideration only shapes gives more of a feeling of true center
// It differs from d2target.BoundingBox which needs to include every visible thing

View file

@ -68,6 +68,172 @@ func Set(g *d2graph.Graph, key string, tag, value *string) (_ *d2graph.Graph, er
return recompile(g)
}
func ReconnectEdge(g *d2graph.Graph, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) {
mk, err := d2parser.ParseMapKey(edgeKey)
if err != nil {
return nil, err
}
if len(mk.Edges) == 0 {
return nil, errors.New("edgeKey must be an edge")
}
if mk.EdgeIndex == nil {
return nil, errors.New("edgeKey must refer to an existing edge")
}
edgeTrimCommon(mk)
obj := g.Root
if mk.Key != nil {
var ok bool
obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return nil, errors.New("edge not found")
}
}
edge, ok := obj.HasEdge(mk)
if !ok {
return nil, errors.New("edge not found")
}
if srcKey != nil {
if edge.Src.AbsID() == *srcKey {
srcKey = nil
}
}
if dstKey != nil {
if edge.Dst.AbsID() == *dstKey {
dstKey = nil
}
}
if srcKey == nil && dstKey == nil {
return g, nil
}
var src *d2graph.Object
var dst *d2graph.Object
if srcKey != nil {
srcmk, err := d2parser.ParseMapKey(*srcKey)
if err != nil {
return nil, err
}
src, ok = g.Root.HasChild(d2graph.Key(srcmk.Key))
if !ok {
return nil, errors.New("newSrc not found")
}
}
if dstKey != nil {
dstmk, err := d2parser.ParseMapKey(*dstKey)
if err != nil {
return nil, err
}
dst, ok = g.Root.HasChild(d2graph.Key(dstmk.Key))
if !ok {
return nil, errors.New("newDst not found")
}
}
ref := edge.References[0]
// for loops where only one end is changing, node is always ensured
if edge.Src != edge.Dst && (srcKey == nil || dstKey == nil) {
var refEdges []*d2ast.Edge
for _, ref := range edge.References {
refEdges = append(refEdges, ref.Edge)
}
if srcKey != nil {
ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Src, true)
}
if dstKey != nil {
ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Dst, false)
}
}
for i := range edge.References {
ref := edge.References[i]
// it's a chain
if len(ref.MapKey.Edges) > 1 && ref.MapKey.EdgeIndex == nil {
splitChain := true
// Changing the start of a chain is okay
if ref.MapKeyEdgeIndex == 0 && dstKey == nil {
splitChain = false
}
// Changing the end of a chain is okay
if ref.MapKeyEdgeIndex == len(ref.MapKey.Edges)-1 && srcKey == nil {
splitChain = false
}
if splitChain {
tmp := *ref.MapKey
mk2 := &tmp
mk2.Edges = []*d2ast.Edge{ref.MapKey.Edges[ref.MapKeyEdgeIndex]}
ref.Scope.InsertAfter(ref.MapKey, mk2)
if ref.MapKeyEdgeIndex < len(ref.MapKey.Edges)-1 {
tmp := *ref.MapKey
mk2 := &tmp
mk2.Edges = ref.MapKey.Edges[ref.MapKeyEdgeIndex+1:]
ref.Scope.InsertAfter(ref.MapKey, mk2)
}
ref.MapKey.Edges = ref.MapKey.Edges[:ref.MapKeyEdgeIndex]
}
}
if src != nil {
srcmk, _ := d2parser.ParseMapKey(*srcKey)
ref.Edge.Src = srcmk.Key
newPath, err := pathFromScopeObj(g, srcmk, ref.ScopeObj)
if err != nil {
return nil, err
}
ref.Edge.Src.Path = newPath
}
if dst != nil {
dstmk, _ := d2parser.ParseMapKey(*dstKey)
ref.Edge.Dst = dstmk.Key
newPath, err := pathFromScopeObj(g, dstmk, ref.ScopeObj)
if err != nil {
return nil, err
}
ref.Edge.Dst.Path = newPath
}
}
return recompile(g)
}
func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) {
ak2 := d2graph.Key(key.Key)
commonPath := getCommonPath(scopeak, ak2)
var newPath []*d2ast.StringBox
// Move out to most common scope
for i := len(commonPath); i < len(scopeak); i++ {
newPath = append(newPath, d2ast.MakeValueBox(d2ast.RawString("_", true)).StringBox())
}
// From most common scope, target the toKey
newPath = append(newPath, key.Key.Path[len(commonPath):]...)
return newPath, nil
}
func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Object) ([]*d2ast.StringBox, error) {
// We don't want this to be underscore-resolved scope. We want to ignore underscores
var scopeak []string
if fromScope != g.Root {
scopek, err := d2parser.ParseKey(fromScope.AbsID())
if err != nil {
return nil, err
}
scopeak = d2graph.Key(scopek)
}
return pathFromScopeKey(g, key, scopeak)
}
func recompile(g *d2graph.Graph) (*d2graph.Graph, error) {
s := d2format.Format(g.AST)
g, err := d2compiler.Compile(g.AST.Range.Path, strings.NewReader(s), nil)
@ -606,7 +772,7 @@ func Delete(g *d2graph.Graph, key string) (_ *d2graph.Graph, err error) {
return nil, err
}
if err := updateNear(prevG, g, &key, nil); err != nil {
if err := updateNear(prevG, g, &key, nil, false); err != nil {
return nil, err
}
@ -753,7 +919,27 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
hoistedAbsKey.Path = append(hoistedAbsKey.Path, ref.Key.Path[:ref.KeyPathIndex]...)
hoistedAbsKey.Path = append(hoistedAbsKey.Path, absKey.Path[len(absKey.Path)-1])
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(hoistedAbsKey), "."), ignored, newIDs)
// Can't generate a key that'd conflict with sibling
var siblingHoistedIDs []string
for _, otherAbsKey := range absKeys {
if absKey == otherAbsKey {
continue
}
ida := d2graph.Key(otherAbsKey)
absKeyStr := strings.Join(ida, ".")
if _, ok := dedupedRenames[absKeyStr]; ok {
continue
}
hoistedAbsKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
if err != nil {
hoistedAbsKey = &d2ast.KeyPath{}
}
hoistedAbsKey.Path = append(hoistedAbsKey.Path, ref.Key.Path[:ref.KeyPathIndex]...)
hoistedAbsKey.Path = append(hoistedAbsKey.Path, otherAbsKey.Path[len(otherAbsKey.Path)-1])
siblingHoistedIDs = append(siblingHoistedIDs, strings.Join(d2graph.Key(hoistedAbsKey), "."))
}
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(hoistedAbsKey), "."), ignored, append(newIDs, siblingHoistedIDs...))
if err != nil {
return nil, err
}
@ -789,7 +975,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
}
for _, k := range renameOrder {
var err error
g, err = move(g, k, renames[k])
g, err = move(g, k, renames[k], false)
if err != nil {
return nil, err
}
@ -1123,12 +1309,12 @@ func ensureNode(g *d2graph.Graph, excludedEdges []*d2ast.Edge, scopeObj *d2graph
}
}
func Rename(g *d2graph.Graph, key, newName string) (_ *d2graph.Graph, err error) {
func Rename(g *d2graph.Graph, key, newName string) (_ *d2graph.Graph, newKey string, err error) {
defer xdefer.Errorf(&err, "failed to rename %#v to %#v", key, newName)
mk, err := d2parser.ParseMapKey(key)
if err != nil {
return nil, err
return nil, "", err
}
if len(mk.Edges) > 0 && mk.EdgeKey == nil {
@ -1136,7 +1322,7 @@ func Rename(g *d2graph.Graph, key, newName string) (_ *d2graph.Graph, err error)
// Maybe we remove Rename and just have Move.
mk2, err := d2parser.ParseMapKey(newName)
if err != nil {
return nil, err
return nil, "", err
}
mk2.Key = mk.Key
@ -1144,13 +1330,25 @@ func Rename(g *d2graph.Graph, key, newName string) (_ *d2graph.Graph, err error)
} else {
_, ok := d2graph.ReservedKeywords[newName]
if ok {
return nil, fmt.Errorf("cannot rename to reserved keyword: %#v", newName)
return nil, "", fmt.Errorf("cannot rename to reserved keyword: %#v", newName)
}
if mk.Key != nil {
obj, ok := g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return nil, "", fmt.Errorf("key does not exist")
}
// If attempt to name something "x", but "x" already exists, rename it "x 2" instead
generatedName, _, err := generateUniqueKey(g, newName, obj, nil)
if err == nil {
newName = generatedName
}
}
// TODO: Handle mk.EdgeKey
mk.Key.Path[len(mk.Key.Path)-1] = d2ast.MakeValueBox(d2ast.RawString(newName, true)).StringBox()
}
return move(g, key, d2format.Format(mk))
g, err = move(g, key, d2format.Format(mk), false)
return g, newName, err
}
func trimReservedSuffix(path []*d2ast.StringBox) []*d2ast.StringBox {
@ -1163,12 +1361,12 @@ func trimReservedSuffix(path []*d2ast.StringBox) []*d2ast.StringBox {
}
// Does not handle edge keys, on account of edge keys can only be reserved, e.g. (a->b).style.color: red
func Move(g *d2graph.Graph, key, newKey string) (_ *d2graph.Graph, err error) {
func Move(g *d2graph.Graph, key, newKey string, includeDescendants bool) (_ *d2graph.Graph, err error) {
defer xdefer.Errorf(&err, "failed to move: %#v to %#v", key, newKey)
return move(g, key, newKey)
return move(g, key, newKey, includeDescendants)
}
func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
func move(g *d2graph.Graph, key, newKey string, includeDescendants bool) (*d2graph.Graph, error) {
if key == newKey {
return g, nil
}
@ -1225,7 +1423,7 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
isCrossScope := strings.Join(ak[:len(ak)-1], ".") != strings.Join(ak2[:len(ak2)-1], ".")
if isCrossScope {
if isCrossScope && !includeDescendants {
g, err = renameConflictsToParent(g, mk.Key)
if err != nil {
return nil, err
@ -1361,7 +1559,14 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
continue
}
firstNonUnderscoreIndex := 0
ida := d2graph.Key(ref.Key)
for i, id := range ida {
if id != "_" {
firstNonUnderscoreIndex = i
break
}
}
resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, ref.ScopeObj)
if err != nil {
return nil, err
@ -1369,6 +1574,8 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
if resolvedObj != obj {
ida = resolvedIDA
}
// e.g. "a.b.shape: circle"
_, endsWithReserved := d2graph.ReservedKeywords[ida[len(ida)-1]]
ida = go2.Filter(ida, func(x string) bool {
_, ok := d2graph.ReservedKeywords[x]
return !ok
@ -1384,14 +1591,16 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
// 4. Slice.
// -- The key is moving from its current scope out to a less nested scope
if isCrossScope {
if len(ida) == 1 {
if (!includeDescendants && len(ida) == 1) || (includeDescendants && ref.KeyPathIndex == firstNonUnderscoreIndex) {
// 1. Transplant
absKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
if err != nil {
absKey = &d2ast.KeyPath{}
}
absKey.Path = append(absKey.Path, ref.Key.Path...)
hoistRefChildren(g, absKey, ref)
if !includeDescendants {
hoistRefChildren(g, absKey, ref)
}
deleteFromMap(ref.Scope, ref.MapKey)
detachedMK := &d2ast.Key{Primary: ref.MapKey.Primary, Key: cloneKey(ref.MapKey.Key)}
detachedMK.Key.Path = go2.Filter(detachedMK.Key.Path, func(x *d2ast.StringBox) bool {
@ -1399,39 +1608,103 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
})
detachedMK.Value = ref.MapKey.Value
if ref.MapKey != nil && ref.MapKey.Value.Map != nil {
detachedMK.Value.Map = &d2ast.Map{
Range: ref.MapKey.Value.Map.Range,
}
for _, n := range ref.MapKey.Value.Map.Nodes {
if n.MapKey == nil {
continue
// Without including descendants, just copy over the reserved
if !includeDescendants {
detachedMK.Value.Map = &d2ast.Map{
Range: ref.MapKey.Value.Map.Range,
}
if n.MapKey.Key != nil {
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
if ok {
detachedMK.Value.Map.Nodes = append(detachedMK.Value.Map.Nodes, n)
for _, n := range ref.MapKey.Value.Map.Nodes {
if n.MapKey == nil {
continue
}
if n.MapKey.Key != nil {
_, ok := d2graph.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
if ok {
detachedMK.Value.Map.Nodes = append(detachedMK.Value.Map.Nodes, n)
}
}
}
if len(detachedMK.Value.Map.Nodes) == 0 {
detachedMK.Value.Map = nil
}
} else {
// Usually copy everything as is when including descendants
// The exception is underscored keys, which need to be updated
for _, n := range ref.MapKey.Value.Map.Nodes {
if n.MapKey == nil {
continue
}
if n.MapKey.Key != nil {
if n.MapKey.Key.Path[0].Unbox().ScalarString() == "_" {
resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(n.MapKey.Key), obj)
if err != nil {
return nil, err
}
newPath, err := pathFromScopeKey(g, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
if err != nil {
return nil, err
}
n.MapKey.Key.Path = newPath
}
}
for _, e := range n.MapKey.Edges {
if e.Src.Path[0].Unbox().ScalarString() == "_" {
resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(e.Src), obj)
if err != nil {
return nil, err
}
newPath, err := pathFromScopeKey(g, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
if err != nil {
return nil, err
}
e.Src.Path = newPath
}
if e.Dst.Path[0].Unbox().ScalarString() == "_" {
resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(e.Dst), obj)
if err != nil {
return nil, err
}
newPath, err := pathFromScopeKey(g, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
if err != nil {
return nil, err
}
e.Dst.Path = newPath
}
}
}
}
if len(detachedMK.Value.Map.Nodes) == 0 {
detachedMK.Value.Map = nil
}
}
appendUniqueMapKey(toScope, detachedMK)
} else if len(ida) > 1 && (!isExplicit || go2.Contains(mostNestedRefs, ref)) {
} else if len(ida) > 1 && (endsWithReserved || !isExplicit || go2.Contains(mostNestedRefs, ref)) {
// 2. Split
detachedMK := &d2ast.Key{Key: cloneKey(ref.MapKey.Key)}
detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
if ref.KeyPathIndex == len(ref.Key.Path)-1 {
if includeDescendants {
detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex:]...)
} else {
detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
}
if includeDescendants {
detachedMK.Value = ref.MapKey.Value
ref.MapKey.Value = d2ast.ValueBox{}
} else if ref.KeyPathIndex == len(filterReservedPath(ref.Key.Path))-1 {
withReserved, withoutReserved := filterReserved(ref.MapKey.Value)
detachedMK.Value = withReserved
ref.MapKey.Value = withoutReserved
detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex:]...)
ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex+1]
}
if includeDescendants {
ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex]
} else {
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
}
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
appendUniqueMapKey(toScope, detachedMK)
} else if len(getCommonPath(ak, ak2)) > 0 {
// 3. Extend
// This case does not make sense for includeDescendants
newKeyPath := ref.Key.Path[:ref.KeyPathIndex]
newKeyPath = append(newKeyPath, mk2.Key.Path[len(getCommonPath(ak, ak2)):]...)
ref.Key.Path = append(newKeyPath, ref.Key.Path[ref.KeyPathIndex+1:]...)
@ -1476,48 +1749,76 @@ func move(g *d2graph.Graph, key, newKey string) (*d2graph.Graph, error) {
continue
}
// We don't want this to be underscore-resolved scope. We want to ignore underscores
var scopeak []string
if ref.ScopeObj != g.Root {
scopek, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
if err != nil {
return nil, err
firstNonUnderscoreIndex := 0
ida := d2graph.Key(ref.Key)
for i, id := range ida {
if id != "_" {
firstNonUnderscoreIndex = i
break
}
scopeak = d2graph.Key(scopek)
}
commonPath := getCommonPath(scopeak, ak2)
// When moving a node out of an edge, e.g. the `b` out of `a.b.c -> ...`,
// The edge needs to continue targeting the same thing (c)
if ref.KeyPathIndex != len(ref.Key.Path)-1 {
// When moving a node out of an edge, e.g. the `b` out of `a.b.c -> ...`,
// The edge needs to continue targeting the same thing (c)
// Split
detachedMK := &d2ast.Key{
Key: cloneKey(ref.Key),
}
detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
appendUniqueMapKey(toScope, detachedMK)
oldPath, err := pathFromScopeObj(g, mk, ref.ScopeObj)
if err != nil {
return nil, err
}
newPath, err := pathFromScopeObj(g, mk2, ref.ScopeObj)
if err != nil {
return nil, err
}
if includeDescendants {
// When including descendants, the only thing that gets dropped, if any, is the uncommon leading path of new key and key
// E.g. when moving `a.b` to `x.b` with edge ref key of `a.b.c`, then changing it to `x.b.c` will drop `a`
diff := len(oldPath) - len(newPath)
// Only need to check uncommon path if the lengths are the same
if diff == 0 {
diff = len(getUncommonPath(d2graph.Key(&d2ast.KeyPath{Path: oldPath}), d2graph.Key(&d2ast.KeyPath{Path: newPath})))
}
// If the old key is longer than the new key, we already know all the diff would be dropped
if diff > 0 && ref.KeyPathIndex != firstNonUnderscoreIndex {
detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex-diff:ref.KeyPathIndex]...)
appendUniqueMapKey(ref.Scope, detachedMK)
}
} else {
detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
appendUniqueMapKey(toScope, detachedMK)
}
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
if includeDescendants {
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex-len(oldPath)+1], append(newPath, ref.Key.Path[ref.KeyPathIndex+1:]...)...)
} else {
ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
}
} else {
// When moving a node connected to an edge, we have to ensure parents continue to exist
// e.g. the `c` out of `a.b.c -> ...`
// `a.b` needs to exist
newPath, err := pathFromScopeObj(g, mk2, ref.ScopeObj)
if err != nil {
return nil, err
}
if len(go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool { return x.Unbox().ScalarString() != "_" })) > 1 {
detachedK := cloneKey(ref.Key)
detachedK.Path = detachedK.Path[:len(detachedK.Path)-1]
ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, detachedK, false)
}
// Move out to most common scope
ref.Key.Path = nil
for i := len(commonPath); i < len(scopeak); i++ {
ref.Key.Path = append(ref.Key.Path, d2ast.MakeValueBox(d2ast.RawString("_", true)).StringBox())
if includeDescendants {
ref.Key.Path = append(newPath, ref.Key.Path[go2.Min(len(ref.Key.Path), ref.KeyPathIndex+len(newPath)):]...)
} else {
ref.Key.Path = newPath
}
// From most common scope, target the toKey
ref.Key.Path = append(ref.Key.Path, mk2.Key.Path[len(commonPath):]...)
}
}
if err := updateNear(prevG, g, &key, &newKey); err != nil {
if err := updateNear(prevG, g, &key, &newKey, includeDescendants); err != nil {
return nil, err
}
@ -1592,7 +1893,7 @@ func filterReserved(value d2ast.ValueBox) (with, without d2ast.ValueBox) {
// updateNear updates all the Near fields
// prevG is the graph before the update (i.e. deletion, rename, move)
func updateNear(prevG, g *d2graph.Graph, from, to *string) error {
func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bool) error {
mk, _ := d2parser.ParseMapKey(*from)
if len(mk.Edges) > 0 {
return nil
@ -1603,6 +1904,13 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string) error {
if len(mk.Key.Path) == 0 {
return nil
}
// TODO get rid of repetition
// Update all the `near` keys that are one level nested
// x: {
// near: z
// }
for _, obj := range g.Objects {
if obj.Map == nil {
continue
@ -1637,7 +1945,7 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string) error {
n.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
}
} else {
deltas, err := MoveIDDeltas(tmpG, *from, *to)
deltas, err := MoveIDDeltas(tmpG, *from, *to, includeDescendants)
if err != nil {
return err
}
@ -1649,6 +1957,52 @@ func updateNear(prevG, g *d2graph.Graph, from, to *string) error {
}
}
}
// Update all the `near` keys that are flat (x.near: z)
for _, obj := range g.Objects {
for _, ref := range obj.References {
if ref.MapKey == nil {
continue
}
if ref.MapKey.Key == nil {
continue
}
if len(ref.MapKey.Key.Path) == 0 {
continue
}
if ref.MapKey.Key.Path[len(ref.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
k := ref.MapKey.Value.ScalarBox().Unbox().ScalarString()
if strings.EqualFold(k, *from) && to == nil {
deleteFromMap(obj.Map, ref.MapKey)
} else {
valueMK, err := d2parser.ParseMapKey(k)
if err != nil {
return err
}
tmpG, _ := recompile(prevG)
appendMapKey(tmpG.AST, valueMK)
if to == nil {
deltas, err := DeleteIDDeltas(tmpG, *from)
if err != nil {
return err
}
if v, ok := deltas[k]; ok {
ref.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
}
} else {
deltas, err := MoveIDDeltas(tmpG, *from, *to, includeDescendants)
if err != nil {
return err
}
if v, ok := deltas[k]; ok {
ref.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
}
}
}
}
}
}
return nil
}
@ -1691,55 +2045,133 @@ func ReparentIDDelta(g *d2graph.Graph, key, parentKey string) (string, error) {
return id, nil
}
func ReconnectEdgeIDDelta(g *d2graph.Graph, edgeID, srcID, dstID string) (string, error) {
mk, err := d2parser.ParseMapKey(edgeID)
func ReconnectEdgeIDDeltas(g *d2graph.Graph, edgeKey string, srcKey, dstKey *string) (deltas map[string]string, err error) {
defer xdefer.Errorf(&err, "failed to get deltas for reconnect edge %#v", edgeKey)
deltas = make(map[string]string)
// Reconnection: nothing is created or destroyed, the edge just gets a new ID
// For deltas, it's indices that change:
// - old sibling edges may decrement index
// -- happens when the edge is not the last edge index
// - new sibling edges may increment index
// -- happens when the edge is not the last edge index
// - new edge of course always needs an entry
// The change happens at the first ref, since that is what changes index
mk, err := d2parser.ParseMapKey(edgeKey)
if err != nil {
return "", err
return nil, err
}
if len(mk.Edges) == 0 {
return nil, errors.New("edgeKey must be an edge")
}
if mk.EdgeIndex == nil {
return nil, errors.New("edgeKey must refer to an existing edge")
}
edgeTrimCommon(mk)
obj := g.Root
if mk.Key != nil {
var ok bool
obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return "", errors.New("edge key not found")
return nil, errors.New("edge not found")
}
}
e, ok := obj.HasEdge(mk)
edge, ok := obj.HasEdge(mk)
if !ok {
return "", fmt.Errorf("edge %v not found", edgeID)
return nil, errors.New("edge not found")
}
if e.Src.AbsID() == srcID && e.Dst.AbsID() == dstID {
return edgeID, nil
if srcKey != nil {
if edge.Src.AbsID() == *srcKey {
srcKey = nil
}
}
oldSrc := e.Src
oldDst := e.Dst
if e.Src.AbsID() != srcID {
mk, err := d2parser.ParseMapKey(srcID)
if dstKey != nil {
if edge.Dst.AbsID() == *dstKey {
dstKey = nil
}
}
if srcKey == nil && dstKey == nil {
return nil, nil
}
newSrc := edge.Src
newDst := edge.Dst
var src *d2graph.Object
var dst *d2graph.Object
if srcKey != nil {
srcmk, err := d2parser.ParseMapKey(*srcKey)
if err != nil {
return "", err
return nil, err
}
src, ok := g.Root.HasChild(d2graph.Key(mk.Key))
src, ok = g.Root.HasChild(d2graph.Key(srcmk.Key))
if !ok {
return "", fmt.Errorf("src %v not found", srcID)
return nil, errors.New("newSrc not found")
}
e.Src = src
newSrc = src
}
if e.Dst.AbsID() != dstID {
mk, err := d2parser.ParseMapKey(dstID)
if dstKey != nil {
dstmk, err := d2parser.ParseMapKey(*dstKey)
if err != nil {
return "", err
return nil, err
}
dst, ok := g.Root.HasChild(d2graph.Key(mk.Key))
dst, ok = g.Root.HasChild(d2graph.Key(dstmk.Key))
if !ok {
return "", fmt.Errorf("dst %v not found", dstID)
return nil, errors.New("newDst not found")
}
e.Dst = dst
newDst = dst
}
newID := fmt.Sprintf("%s %s %s", e.Src.AbsID(), e.ArrowString(), e.Dst.AbsID())
e.Src = oldSrc
e.Dst = oldDst
return newID, nil
// The first ref is always the definition
firstRef := edge.References[0]
line := firstRef.MapKey.Range.Start.Line
newIndex := 0
// For the edge's own delta, it just needs to know how many edges came before it with the same src and dst
for _, otherEdge := range g.Edges {
if otherEdge.Src == newSrc && otherEdge.Dst == newDst {
firstRef := otherEdge.References[0]
if firstRef.MapKey.Range.Start.Line <= line {
newIndex++
}
}
if otherEdge.Src == edge.Src && otherEdge.Dst == edge.Dst && otherEdge.Index > edge.Index {
before := otherEdge.AbsID()
otherEdge.Index--
after := otherEdge.AbsID()
deltas[before] = after
otherEdge.Index++
}
}
for _, otherEdge := range g.Edges {
if otherEdge.Src == newSrc && otherEdge.Dst == newDst {
if otherEdge.Index >= newIndex {
before := otherEdge.AbsID()
otherEdge.Index++
after := otherEdge.AbsID()
deltas[before] = after
otherEdge.Index--
}
}
}
newEdge := &d2graph.Edge{
Src: newSrc,
Dst: newDst,
SrcArrow: edge.SrcArrow,
DstArrow: edge.DstArrow,
Index: newIndex,
}
deltas[edge.AbsID()] = newEdge.AbsID()
return deltas, nil
}
// generateUniqueKey generates a unique key by appending a number after `prefix` such that it doesn't conflict with any IDs in `g`
@ -1848,6 +2280,16 @@ func getCommonPath(a, b []string) []string {
return out
}
func getUncommonPath(a, b []string) []string {
var out []string
for i := 0; i < len(a) && i < len(b); i++ {
if a[i] != b[i] {
out = a[:i+1]
}
}
return out
}
func edgeTrimCommon(mk *d2ast.Key) {
if len(mk.Edges) != 1 {
return
@ -1866,7 +2308,7 @@ func edgeTrimCommon(mk *d2ast.Key) {
}
}
func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]string, err error) {
func MoveIDDeltas(g *d2graph.Graph, key, newKey string, includeDescendants bool) (deltas map[string]string, err error) {
defer xdefer.Errorf(&err, "failed to get deltas for move from %#v to %#v", key, newKey)
deltas = make(map[string]string)
@ -1915,48 +2357,50 @@ func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]strin
}
}
for _, ch := range obj.ChildrenArray {
chMK, err := d2parser.ParseMapKey(ch.AbsID())
if err != nil {
return nil, err
}
ida := d2graph.Key(chMK.Key)
if ida[len(ida)-1] == ida[len(ida)-2] {
continue
}
hoistedAbsID := ch.ID
if obj.Parent != g.Root {
hoistedAbsID = obj.Parent.AbsID() + "." + ch.ID
}
hoistedMK, err := d2parser.ParseMapKey(hoistedAbsID)
if err != nil {
return nil, err
}
conflictsWithNewID := false
for _, id := range newIDs {
if id == d2format.Format(hoistedMK.Key) {
conflictsWithNewID = true
break
}
}
if _, ok := g.Root.HasChild(d2graph.Key(hoistedMK.Key)); ok || conflictsWithNewID {
newKey, _, err := generateUniqueKey(g, hoistedAbsID, ignored, newIDs)
if !includeDescendants {
for _, ch := range obj.ChildrenArray {
chMK, err := d2parser.ParseMapKey(ch.AbsID())
if err != nil {
return nil, err
}
newMK, err := d2parser.ParseMapKey(newKey)
ida := d2graph.Key(chMK.Key)
if ida[len(ida)-1] == ida[len(ida)-2] {
continue
}
hoistedAbsID := ch.ID
if obj.Parent != g.Root {
hoistedAbsID = obj.Parent.AbsID() + "." + ch.ID
}
hoistedMK, err := d2parser.ParseMapKey(hoistedAbsID)
if err != nil {
return nil, err
}
newAK := d2graph.Key(newMK.Key)
conflictOldIDs[ch] = ch.ID
conflictNewIDs[ch] = newAK[len(newAK)-1]
newIDs = append(newIDs, d2format.Format(newMK.Key))
} else {
newIDs = append(newIDs, d2format.Format(hoistedMK.Key))
conflictsWithNewID := false
for _, id := range newIDs {
if id == d2format.Format(hoistedMK.Key) {
conflictsWithNewID = true
break
}
}
if _, ok := g.Root.HasChild(d2graph.Key(hoistedMK.Key)); ok || conflictsWithNewID {
newKey, _, err := generateUniqueKey(g, hoistedAbsID, ignored, newIDs)
if err != nil {
return nil, err
}
newMK, err := d2parser.ParseMapKey(newKey)
if err != nil {
return nil, err
}
newAK := d2graph.Key(newMK.Key)
conflictOldIDs[ch] = ch.ID
conflictNewIDs[ch] = newAK[len(newAK)-1]
newIDs = append(newIDs, d2format.Format(newMK.Key))
} else {
newIDs = append(newIDs, d2format.Format(hoistedMK.Key))
}
}
}
}
@ -1998,7 +2442,7 @@ func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]strin
id := ak2[len(ak2)-1]
tmpRenames := func() func() {
if isCrossScope {
if isCrossScope && !includeDescendants {
for _, ch := range obj.ChildrenArray {
ch.Parent = obj.Parent
}
@ -2018,7 +2462,7 @@ func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]strin
obj.ID = beforeObjID
obj.Parent = prevParent
if isCrossScope {
if isCrossScope && !includeDescendants {
for _, ch := range obj.ChildrenArray {
ch.Parent = obj
}
@ -2291,7 +2735,7 @@ func RenameIDDeltas(g *d2graph.Graph, key, newName string) (deltas map[string]st
}
mk.Key.Path[len(mk.Key.Path)-1].Unbox().SetString(newName)
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(mk.Key), "."), nil, nil)
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(mk.Key), "."), obj, nil)
if err != nil {
return nil, err
}
@ -2305,19 +2749,23 @@ func RenameIDDeltas(g *d2graph.Graph, key, newName string) (deltas map[string]st
beforeObjID := obj.ID
appendNodeDelta := func(ch *d2graph.Object) {
beforeID := ch.AbsID()
obj.ID = newNameKey
deltas[beforeID] = ch.AbsID()
obj.ID = beforeObjID
if obj.ID != newNameKey {
beforeID := ch.AbsID()
obj.ID = newNameKey
deltas[beforeID] = ch.AbsID()
obj.ID = beforeObjID
}
}
appendEdgeDelta := func(ch *d2graph.Object) {
for _, e := range obj.Graph.Edges {
if e.Src == ch || e.Dst == ch {
beforeID := e.AbsID()
obj.ID = newNameKey
deltas[beforeID] = e.AbsID()
obj.ID = beforeObjID
if obj.ID != newNameKey {
beforeID := e.AbsID()
obj.ID = newNameKey
deltas[beforeID] = e.AbsID()
obj.ID = beforeObjID
}
}
}
}
@ -2391,3 +2839,13 @@ func getMostNestedRefs(obj *d2graph.Object) []d2graph.Reference {
return out
}
func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
for _, box := range path {
if _, ok := d2graph.ReservedKeywords[strings.ToLower(box.Unbox().ScalarString())]; ok {
return
}
filtered = append(filtered, box)
}
return
}

File diff suppressed because it is too large Load diff

View file

@ -3,10 +3,29 @@ package d2oracle
import (
"fmt"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
)
func GetChildrenIDs(g *d2graph.Graph, absID string) (ids []string, _ error) {
mk, err := d2parser.ParseMapKey(absID)
if err != nil {
return nil, err
}
obj, ok := g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return nil, fmt.Errorf("%v not found", absID)
}
for _, ch := range obj.ChildrenArray {
ids = append(ids, ch.AbsID())
}
return ids, nil
}
func GetParentID(g *d2graph.Graph, absID string) (string, error) {
mk, err := d2parser.ParseMapKey(absID)
if err != nil {
@ -35,6 +54,24 @@ func GetEdge(g *d2graph.Graph, absID string) *d2graph.Edge {
return nil
}
func GetObjOrder(g *d2graph.Graph) []string {
var order []string
queue := []*d2graph.Object{g.Root}
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
if curr != g.Root {
order = append(order, curr.AbsID())
}
for _, ch := range curr.ChildrenArray {
queue = append(queue, ch)
}
}
return order
}
func IsLabelKeyID(key, label string) bool {
mk, err := d2parser.ParseMapKey(key)
if err != nil {
@ -49,3 +86,18 @@ func IsLabelKeyID(key, label string) bool {
return mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString() == label
}
func GetID(key string) string {
mk, err := d2parser.ParseMapKey(key)
if err != nil {
return ""
}
if len(mk.Edges) > 0 {
return ""
}
if mk.Key == nil {
return ""
}
return d2format.Format(d2ast.RawString(mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString(), true))
}

View file

@ -18,3 +18,9 @@ func TestIsLabelKeyID(t *testing.T) {
assert.Equal(t, false, d2oracle.IsLabelKeyID("x", "y"))
assert.Equal(t, false, d2oracle.IsLabelKeyID("x->y", "y"))
}
func TestGetID(t *testing.T) {
t.Parallel()
assert.Equal(t, `"y (z)"`, d2oracle.GetID(`y (z)`))
}

View file

@ -919,11 +919,12 @@ func (p *parser) parseKey() (k *d2ast.KeyPath) {
Start: p.pos,
},
}
defer k.Range.End.From(&p.pos)
defer func() {
if len(k.Path) == 0 {
k = nil
} else {
k.Range.End = k.Path[len(k.Path)-1].Unbox().GetRange().End
}
}()
@ -948,6 +949,9 @@ func (p *parser) parseKey() (k *d2ast.KeyPath) {
return k
}
if len(k.Path) == 0 {
k.Range.Start = s.GetRange().Start
}
k.Path = append(k.Path, &sb)
r, newlines, eof = p.peekNotSpace()

View file

@ -20,8 +20,9 @@ func TestParse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
text string
name string
text string
assert func(t testing.TB, ast *d2ast.Map, err error)
// exp is in testdata/d2parser/TestParse/${name}.json
}{
@ -379,6 +380,18 @@ b-
c-
`,
},
{
name: "whitespace_range",
text: ` a -> b -> c `,
assert: func(t testing.TB, ast *d2ast.Map, err error) {
assert.Equal(t, "1:2", ast.Nodes[0].MapKey.Edges[0].Src.Range.Start.String())
assert.Equal(t, "1:3", ast.Nodes[0].MapKey.Edges[0].Src.Range.End.String())
assert.Equal(t, "1:7", ast.Nodes[0].MapKey.Edges[0].Dst.Range.Start.String())
assert.Equal(t, "1:8", ast.Nodes[0].MapKey.Edges[0].Dst.Range.End.String())
assert.Equal(t, "1:12", ast.Nodes[0].MapKey.Edges[1].Dst.Range.Start.String())
assert.Equal(t, "1:13", ast.Nodes[0].MapKey.Edges[1].Dst.Range.End.String())
},
},
}
for _, tc := range testCases {
@ -389,6 +402,10 @@ c-
d2Path := fmt.Sprintf("d2/testdata/d2parser/%v.d2", t.Name())
ast, err := d2parser.Parse(d2Path, strings.NewReader(tc.text), nil)
if tc.assert != nil {
tc.assert(t, ast, err)
}
got := struct {
AST *d2ast.Map `json:"ast"`
Err error `json:"err"`

View file

@ -10,8 +10,7 @@ import (
"fmt"
"strings"
"github.com/jung-kurt/gofpdf"
"oss.terrastruct.com/d2/lib/font"
fontlib "oss.terrastruct.com/d2/lib/font"
)
@ -44,7 +43,7 @@ func (f Font) GetEncodedSubset(corpus string) string {
fontBuf := make([]byte, len(FontFaces[f]))
copy(fontBuf, FontFaces[f])
fontBuf = gofpdf.UTF8CutFont(fontBuf, uniqueChars)
fontBuf = font.UTF8CutFont(fontBuf, uniqueChars)
fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
if err != nil {

View file

@ -4,8 +4,7 @@ import (
"path/filepath"
"testing"
"github.com/jung-kurt/gofpdf"
"oss.terrastruct.com/d2/lib/font"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
)
@ -17,7 +16,7 @@ func TestCutFont(t *testing.T) {
}
fontBuf := make([]byte, len(FontFaces[f]))
copy(fontBuf, FontFaces[f])
fontBuf = gofpdf.UTF8CutFont(fontBuf, " 1")
fontBuf = font.UTF8CutFont(fontBuf, " 1")
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
assert.Success(t, err)
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 161 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 493 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 676 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-916646398" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 304 407"><svg id="d2-svg" class="d2-916646398" width="304" height="407" viewBox="-101 -118 304 407"><rect x="-101.000000" y="-118.000000" width="304" height="407" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}
@ -7,7 +7,7 @@
}
@font-face {
font-family: d2-916646398-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 657 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 660 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 660 KiB

View file

@ -642,7 +642,7 @@ func defineShadowFilter(writer io.Writer) {
</defs>`)
}
func render3dRect(targetShape d2target.Shape) string {
func render3DRect(targetShape d2target.Shape) string {
moveTo := func(p d2target.Point) string {
return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
@ -738,7 +738,7 @@ func render3dRect(targetShape d2target.Shape) string {
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
func render3dHexagon(targetShape d2target.Shape) string {
func render3DHexagon(targetShape d2target.Shape) string {
moveTo := func(p d2target.Point) string {
return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
@ -995,7 +995,7 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
borderRadius = float64(targetShape.BorderRadius)
}
if targetShape.ThreeDee {
fmt.Fprint(writer, render3dRect(targetShape))
fmt.Fprint(writer, render3DRect(targetShape))
} else {
if !targetShape.DoubleBorder {
if targetShape.Multiple {
@ -1088,7 +1088,7 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
}
case d2target.ShapeHexagon:
if targetShape.ThreeDee {
fmt.Fprint(writer, render3dHexagon(targetShape))
fmt.Fprint(writer, render3DHexagon(targetShape))
} else {
if targetShape.Multiple {
multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
@ -1186,7 +1186,19 @@ func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape,
labelPosition := label.Position(targetShape.LabelPosition)
var box *geo.Box
if labelPosition.IsOutside() {
box = s.GetBox()
box = s.GetBox().Copy()
// if it is 3d/multiple, place label using box around those
if targetShape.ThreeDee {
offsetY := d2target.THREE_DEE_OFFSET
if targetShape.Type == d2target.ShapeHexagon {
offsetY /= 2
}
box.TopLeft.Y -= float64(offsetY)
box.Width += d2target.THREE_DEE_OFFSET
} else if targetShape.Multiple {
box.TopLeft.Y -= d2target.MULTIPLE_OFFSET
box.Width += d2target.MULTIPLE_OFFSET
}
} else {
box = s.GetInnerBox()
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -22,6 +22,8 @@ const (
DEFAULT_ICON_SIZE = 32
MAX_ICON_SIZE = 64
SHADOW_SIZE_X = 3
SHADOW_SIZE_Y = 5
THREE_DEE_OFFSET = 15
MULTIPLE_OFFSET = 10
@ -171,9 +173,17 @@ func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
y1 = go2.Min(y1, targetShape.Pos.Y-targetShape.StrokeWidth-16)
x2 = go2.Max(x2, targetShape.Pos.X+targetShape.StrokeWidth+targetShape.Width+16)
}
if targetShape.Shadow {
y2 = go2.Max(y2, targetShape.Pos.Y+targetShape.Height+int(math.Ceil(float64(targetShape.StrokeWidth)/2.))+SHADOW_SIZE_Y)
x2 = go2.Max(x2, targetShape.Pos.X+targetShape.Width+int(math.Ceil(float64(targetShape.StrokeWidth)/2.))+SHADOW_SIZE_X)
}
if targetShape.ThreeDee {
y1 = go2.Min(y1, targetShape.Pos.Y-THREE_DEE_OFFSET-targetShape.StrokeWidth)
offsetY := THREE_DEE_OFFSET
if targetShape.Type == ShapeHexagon {
offsetY /= 2
}
y1 = go2.Min(y1, targetShape.Pos.Y-offsetY-targetShape.StrokeWidth)
x2 = go2.Max(x2, targetShape.Pos.X+THREE_DEE_OFFSET+targetShape.Width+targetShape.StrokeWidth)
}
if targetShape.Multiple {
@ -853,6 +863,8 @@ func init() {
for k, v := range DSL_SHAPE_TO_SHAPE_TYPE {
SHAPE_TYPE_TO_DSL_SHAPE[v] = k
}
// SQUARE_TYPE is defined twice in the map, make sure it doesn't get set to the empty string one
SHAPE_TYPE_TO_DSL_SHAPE[shape.SQUARE_TYPE] = ShapeRectangle
}
func GetIconSize(box *geo.Box, position string) int {

View file

@ -28,7 +28,7 @@ func main() {
// Create a shape with the ID, "cat"
graph, _, _ = d2oracle.Create(graph, "cat")
// Move the shape "meow" inside the container "cat"
graph, _ = d2oracle.Move(graph, "meow", "cat.meow")
graph, _ = d2oracle.Move(graph, "meow", "cat.meow", false)
// Prints formatted D2 script
fmt.Print(d2format.Format(graph.AST))
}

View file

@ -5,6 +5,7 @@ import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
@ -76,6 +77,30 @@ func TestCLI_E2E(t *testing.T) {
assert.TestdataDir(t, filepath.Join(dir, "empty-layer"))
},
},
{
// Skip the empty base board so the animation doesn't show blank for 1400ms
name: "empty-base",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "empty-base.d2", `steps: {
1: {
a -> b
}
2: {
b -> d
c -> d
}
3: {
d -> e
}
}`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "empty-base.d2")
assert.Success(t, err)
svg := readFile(t, dir, "empty-base.svg")
assert.Testdata(t, ".svg", svg)
assert.Equal(t, 3, getNumBoards(string(svg)))
},
},
{
name: "animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
@ -447,3 +472,9 @@ func removeD2Files(tb testing.TB, dir string) {
func testdataIgnoreDiff(tb testing.TB, ext string, got []byte) {
_ = diff.Testdata(filepath.Join("testdata", tb.Name()), ext, got)
}
// getNumBoards gets the number of boards in an SVG file through a non-robust pattern search
// If the renderer changes, this must change
func getNumBoards(svg string) int {
return strings.Count(svg, `class="d2`)
}

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-855222762" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-855222762" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-855222762 .text-bold {
font-family: "d2-855222762-font-bold";
}
@font-face {
font-family: d2-855222762-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 514 665"><svg id="d2-svg" width="514" height="665" viewBox="-206 -166 514 665"><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 514 665"><svg id="d2-svg" width="514" height="665" viewBox="-206 -166 514 665"><style type="text/css"><![CDATA[
.d2-508224771 .text {
font-family: "d2-508224771-font-regular";
}
@font-face {
font-family: d2-508224771-font-regular;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}
.d2-508224771 .text-bold {
font-family: "d2-508224771-font-bold";
}
@font-face {
font-family: d2-508224771-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-855222762" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-855222762" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-855222762 .text-bold {
font-family: "d2-855222762-font-bold";
}
@font-face {
font-family: d2-855222762-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 368 766"><svg id="d2-svg" width="368" height="766" viewBox="-101 -101 368 766"><style type="text/css"><![CDATA[
.d2-1644916896 .text-bold {
font-family: "d2-1644916896-font-bold";
}
@font-face {
font-family: d2-1644916896-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-1644916896 .fill-N1{fill:#0A0F25;}
.d2-1644916896 .fill-N2{fill:#676C7E;}
.d2-1644916896 .fill-N3{fill:#9499AB;}
.d2-1644916896 .fill-N4{fill:#CFD2DD;}
.d2-1644916896 .fill-N5{fill:#DEE1EB;}
.d2-1644916896 .fill-N6{fill:#EEF1F8;}
.d2-1644916896 .fill-N7{fill:#FFFFFF;}
.d2-1644916896 .fill-B1{fill:#0D32B2;}
.d2-1644916896 .fill-B2{fill:#0D32B2;}
.d2-1644916896 .fill-B3{fill:#E3E9FD;}
.d2-1644916896 .fill-B4{fill:#E3E9FD;}
.d2-1644916896 .fill-B5{fill:#EDF0FD;}
.d2-1644916896 .fill-B6{fill:#F7F8FE;}
.d2-1644916896 .fill-AA2{fill:#4A6FF3;}
.d2-1644916896 .fill-AA4{fill:#EDF0FD;}
.d2-1644916896 .fill-AA5{fill:#F7F8FE;}
.d2-1644916896 .fill-AB4{fill:#EDF0FD;}
.d2-1644916896 .fill-AB5{fill:#F7F8FE;}
.d2-1644916896 .stroke-N1{stroke:#0A0F25;}
.d2-1644916896 .stroke-N2{stroke:#676C7E;}
.d2-1644916896 .stroke-N3{stroke:#9499AB;}
.d2-1644916896 .stroke-N4{stroke:#CFD2DD;}
.d2-1644916896 .stroke-N5{stroke:#DEE1EB;}
.d2-1644916896 .stroke-N6{stroke:#EEF1F8;}
.d2-1644916896 .stroke-N7{stroke:#FFFFFF;}
.d2-1644916896 .stroke-B1{stroke:#0D32B2;}
.d2-1644916896 .stroke-B2{stroke:#0D32B2;}
.d2-1644916896 .stroke-B3{stroke:#E3E9FD;}
.d2-1644916896 .stroke-B4{stroke:#E3E9FD;}
.d2-1644916896 .stroke-B5{stroke:#EDF0FD;}
.d2-1644916896 .stroke-B6{stroke:#F7F8FE;}
.d2-1644916896 .stroke-AA2{stroke:#4A6FF3;}
.d2-1644916896 .stroke-AA4{stroke:#EDF0FD;}
.d2-1644916896 .stroke-AA5{stroke:#F7F8FE;}
.d2-1644916896 .stroke-AB4{stroke:#EDF0FD;}
.d2-1644916896 .stroke-AB5{stroke:#F7F8FE;}
.d2-1644916896 .background-color-N1{background-color:#0A0F25;}
.d2-1644916896 .background-color-N2{background-color:#676C7E;}
.d2-1644916896 .background-color-N3{background-color:#9499AB;}
.d2-1644916896 .background-color-N4{background-color:#CFD2DD;}
.d2-1644916896 .background-color-N5{background-color:#DEE1EB;}
.d2-1644916896 .background-color-N6{background-color:#EEF1F8;}
.d2-1644916896 .background-color-N7{background-color:#FFFFFF;}
.d2-1644916896 .background-color-B1{background-color:#0D32B2;}
.d2-1644916896 .background-color-B2{background-color:#0D32B2;}
.d2-1644916896 .background-color-B3{background-color:#E3E9FD;}
.d2-1644916896 .background-color-B4{background-color:#E3E9FD;}
.d2-1644916896 .background-color-B5{background-color:#EDF0FD;}
.d2-1644916896 .background-color-B6{background-color:#F7F8FE;}
.d2-1644916896 .background-color-AA2{background-color:#4A6FF3;}
.d2-1644916896 .background-color-AA4{background-color:#EDF0FD;}
.d2-1644916896 .background-color-AA5{background-color:#F7F8FE;}
.d2-1644916896 .background-color-AB4{background-color:#EDF0FD;}
.d2-1644916896 .background-color-AB5{background-color:#F7F8FE;}
.d2-1644916896 .color-N1{color:#0A0F25;}
.d2-1644916896 .color-N2{color:#676C7E;}
.d2-1644916896 .color-N3{color:#9499AB;}
.d2-1644916896 .color-N4{color:#CFD2DD;}
.d2-1644916896 .color-N5{color:#DEE1EB;}
.d2-1644916896 .color-N6{color:#EEF1F8;}
.d2-1644916896 .color-N7{color:#FFFFFF;}
.d2-1644916896 .color-B1{color:#0D32B2;}
.d2-1644916896 .color-B2{color:#0D32B2;}
.d2-1644916896 .color-B3{color:#E3E9FD;}
.d2-1644916896 .color-B4{color:#E3E9FD;}
.d2-1644916896 .color-B5{color:#EDF0FD;}
.d2-1644916896 .color-B6{color:#F7F8FE;}
.d2-1644916896 .color-AA2{color:#4A6FF3;}
.d2-1644916896 .color-AA4{color:#EDF0FD;}
.d2-1644916896 .color-AA5{color:#F7F8FE;}
.d2-1644916896 .color-AB4{color:#EDF0FD;}
.d2-1644916896 .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}]]></style><style type="text/css"><![CDATA[@keyframes d2Transition-d2-1644916896-0 {
0%, 0.000000% {
opacity: 0;
}
0.000000%, 33.309524% {
opacity: 1;
}
33.333333%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1644916896-1 {
0%, 33.309524% {
opacity: 0;
}
33.333333%, 66.642857% {
opacity: 1;
}
66.666667%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1644916896-2 {
0%, 66.642857% {
opacity: 0;
}
66.666667%, 100.000000% {
opacity: 1;
}
}]]></style><g style="animation: d2Transition-d2-1644916896-0 4200ms infinite" class="d2-1644916896" width="255" height="434" viewBox="-101 -101 255 434"><rect x="-101.000000" y="-101.000000" width="255.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">a</text></g><g id="b"><g class="shape" ><rect x="0.000000" y="166.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">b</text></g><g id="(a -&gt; b)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 26.500000 68.000000 C 26.500000 106.000000 26.500000 126.000000 26.500000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-746933975)" /></g><mask id="d2-746933975" maskUnits="userSpaceOnUse" x="-101" y="-101" width="255" height="434">
<rect x="-101" y="-101" width="255" height="434" fill="white"></rect>
</mask></g><g style="animation: d2Transition-d2-1644916896-1 4200ms infinite" class="d2-1644916896" width="368" height="600" viewBox="-101 -101 368 600"><rect x="-101.000000" y="-101.000000" width="368.000000" height="600.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">a</text></g><g id="b"><g class="shape" ><rect x="0.000000" y="166.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">b</text></g><g id="d"><g class="shape" ><rect x="56.000000" y="332.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="83.000000" y="370.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">d</text></g><g id="c"><g class="shape" ><rect x="113.000000" y="166.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="139.500000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">c</text></g><g id="(a -&gt; b)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 26.500000 68.000000 C 26.500000 106.000000 26.500000 126.000000 26.500000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-919984524)" /></g><g id="(b -&gt; d)[0]"><path d="M 26.500000 234.000000 C 26.500000 272.000000 33.299999 292.000000 58.250760 328.692294" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-919984524)" /></g><g id="(c -&gt; d)[0]"><path d="M 139.500000 234.000000 C 139.500000 272.000000 132.699997 292.000000 107.749240 328.692294" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-919984524)" /></g><mask id="d2-919984524" maskUnits="userSpaceOnUse" x="-101" y="-101" width="368" height="600">
<rect x="-101" y="-101" width="368" height="600" fill="white"></rect>
</mask></g><g style="animation: d2Transition-d2-1644916896-2 4200ms infinite" class="d2-1644916896" width="368" height="766" viewBox="-101 -101 368 766"><rect x="-101.000000" y="-101.000000" width="368.000000" height="766.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="a"><g class="shape" ><rect x="0.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">a</text></g><g id="b"><g class="shape" ><rect x="0.000000" y="166.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="26.500000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">b</text></g><g id="d"><g class="shape" ><rect x="56.000000" y="332.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="83.000000" y="370.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">d</text></g><g id="c"><g class="shape" ><rect x="113.000000" y="166.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="139.500000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">c</text></g><g id="e"><g class="shape" ><rect x="57.000000" y="498.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="83.500000" y="536.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">e</text></g><g id="(a -&gt; b)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 26.500000 68.000000 C 26.500000 106.000000 26.500000 126.000000 26.500000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-348119987)" /></g><g id="(b -&gt; d)[0]"><path d="M 26.500000 234.000000 C 26.500000 272.000000 33.299999 292.000000 58.250760 328.692294" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-348119987)" /></g><g id="(c -&gt; d)[0]"><path d="M 139.500000 234.000000 C 139.500000 272.000000 132.699997 292.000000 107.749240 328.692294" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-348119987)" /></g><g id="(d -&gt; e)[0]"><path d="M 83.000000 400.000000 C 83.000000 438.000000 83.000000 458.000000 83.000000 494.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-348119987)" /></g><mask id="d2-348119987" maskUnits="userSpaceOnUse" x="-101" y="-101" width="368" height="766">
<rect x="-101" y="-101" width="368" height="766" fill="white"></rect>
</mask></g></svg></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 200 200"><svg id="d2-svg" class="d2-121760133" width="200" height="200" viewBox="-100 -100 200 200"><rect x="-100.000000" y="-100.000000" width="200.000000" height="200.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[]]></style><style type="text/css"><![CDATA[.shape {
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 200 200"><svg id="d2-svg" class="d2-121760133" width="200" height="200" viewBox="-100 -100 200 200"><rect x="-100.000000" y="-100.000000" width="200.000000" height="200.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-803513315" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-803513315 .text-bold {
font-family: "d2-803513315-font-bold";
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-1380559207" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1380559207 .text-bold {
font-family: "d2-1380559207-font-bold";
}
@font-face {
font-family: d2-803513315-font-bold;
src: url("data:application/font-woff;base64,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");
font-family: d2-1380559207-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
@ -18,78 +18,78 @@
opacity: 0.5;
}
.d2-803513315 .fill-N1{fill:#0A0F25;}
.d2-803513315 .fill-N2{fill:#676C7E;}
.d2-803513315 .fill-N3{fill:#9499AB;}
.d2-803513315 .fill-N4{fill:#CFD2DD;}
.d2-803513315 .fill-N5{fill:#DEE1EB;}
.d2-803513315 .fill-N6{fill:#EEF1F8;}
.d2-803513315 .fill-N7{fill:#FFFFFF;}
.d2-803513315 .fill-B1{fill:#0D32B2;}
.d2-803513315 .fill-B2{fill:#0D32B2;}
.d2-803513315 .fill-B3{fill:#E3E9FD;}
.d2-803513315 .fill-B4{fill:#E3E9FD;}
.d2-803513315 .fill-B5{fill:#EDF0FD;}
.d2-803513315 .fill-B6{fill:#F7F8FE;}
.d2-803513315 .fill-AA2{fill:#4A6FF3;}
.d2-803513315 .fill-AA4{fill:#EDF0FD;}
.d2-803513315 .fill-AA5{fill:#F7F8FE;}
.d2-803513315 .fill-AB4{fill:#EDF0FD;}
.d2-803513315 .fill-AB5{fill:#F7F8FE;}
.d2-803513315 .stroke-N1{stroke:#0A0F25;}
.d2-803513315 .stroke-N2{stroke:#676C7E;}
.d2-803513315 .stroke-N3{stroke:#9499AB;}
.d2-803513315 .stroke-N4{stroke:#CFD2DD;}
.d2-803513315 .stroke-N5{stroke:#DEE1EB;}
.d2-803513315 .stroke-N6{stroke:#EEF1F8;}
.d2-803513315 .stroke-N7{stroke:#FFFFFF;}
.d2-803513315 .stroke-B1{stroke:#0D32B2;}
.d2-803513315 .stroke-B2{stroke:#0D32B2;}
.d2-803513315 .stroke-B3{stroke:#E3E9FD;}
.d2-803513315 .stroke-B4{stroke:#E3E9FD;}
.d2-803513315 .stroke-B5{stroke:#EDF0FD;}
.d2-803513315 .stroke-B6{stroke:#F7F8FE;}
.d2-803513315 .stroke-AA2{stroke:#4A6FF3;}
.d2-803513315 .stroke-AA4{stroke:#EDF0FD;}
.d2-803513315 .stroke-AA5{stroke:#F7F8FE;}
.d2-803513315 .stroke-AB4{stroke:#EDF0FD;}
.d2-803513315 .stroke-AB5{stroke:#F7F8FE;}
.d2-803513315 .background-color-N1{background-color:#0A0F25;}
.d2-803513315 .background-color-N2{background-color:#676C7E;}
.d2-803513315 .background-color-N3{background-color:#9499AB;}
.d2-803513315 .background-color-N4{background-color:#CFD2DD;}
.d2-803513315 .background-color-N5{background-color:#DEE1EB;}
.d2-803513315 .background-color-N6{background-color:#EEF1F8;}
.d2-803513315 .background-color-N7{background-color:#FFFFFF;}
.d2-803513315 .background-color-B1{background-color:#0D32B2;}
.d2-803513315 .background-color-B2{background-color:#0D32B2;}
.d2-803513315 .background-color-B3{background-color:#E3E9FD;}
.d2-803513315 .background-color-B4{background-color:#E3E9FD;}
.d2-803513315 .background-color-B5{background-color:#EDF0FD;}
.d2-803513315 .background-color-B6{background-color:#F7F8FE;}
.d2-803513315 .background-color-AA2{background-color:#4A6FF3;}
.d2-803513315 .background-color-AA4{background-color:#EDF0FD;}
.d2-803513315 .background-color-AA5{background-color:#F7F8FE;}
.d2-803513315 .background-color-AB4{background-color:#EDF0FD;}
.d2-803513315 .background-color-AB5{background-color:#F7F8FE;}
.d2-803513315 .color-N1{color:#0A0F25;}
.d2-803513315 .color-N2{color:#676C7E;}
.d2-803513315 .color-N3{color:#9499AB;}
.d2-803513315 .color-N4{color:#CFD2DD;}
.d2-803513315 .color-N5{color:#DEE1EB;}
.d2-803513315 .color-N6{color:#EEF1F8;}
.d2-803513315 .color-N7{color:#FFFFFF;}
.d2-803513315 .color-B1{color:#0D32B2;}
.d2-803513315 .color-B2{color:#0D32B2;}
.d2-803513315 .color-B3{color:#E3E9FD;}
.d2-803513315 .color-B4{color:#E3E9FD;}
.d2-803513315 .color-B5{color:#EDF0FD;}
.d2-803513315 .color-B6{color:#F7F8FE;}
.d2-803513315 .color-AA2{color:#4A6FF3;}
.d2-803513315 .color-AA4{color:#EDF0FD;}
.d2-803513315 .color-AA5{color:#F7F8FE;}
.d2-803513315 .color-AB4{color:#EDF0FD;}
.d2-803513315 .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}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 27.000000 68.000000 C 27.000000 106.000000 27.000000 126.000000 27.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-803513315)" /></g><mask id="d2-803513315" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
.d2-1380559207 .fill-N1{fill:#0A0F25;}
.d2-1380559207 .fill-N2{fill:#676C7E;}
.d2-1380559207 .fill-N3{fill:#9499AB;}
.d2-1380559207 .fill-N4{fill:#CFD2DD;}
.d2-1380559207 .fill-N5{fill:#DEE1EB;}
.d2-1380559207 .fill-N6{fill:#EEF1F8;}
.d2-1380559207 .fill-N7{fill:#FFFFFF;}
.d2-1380559207 .fill-B1{fill:#0D32B2;}
.d2-1380559207 .fill-B2{fill:#0D32B2;}
.d2-1380559207 .fill-B3{fill:#E3E9FD;}
.d2-1380559207 .fill-B4{fill:#E3E9FD;}
.d2-1380559207 .fill-B5{fill:#EDF0FD;}
.d2-1380559207 .fill-B6{fill:#F7F8FE;}
.d2-1380559207 .fill-AA2{fill:#4A6FF3;}
.d2-1380559207 .fill-AA4{fill:#EDF0FD;}
.d2-1380559207 .fill-AA5{fill:#F7F8FE;}
.d2-1380559207 .fill-AB4{fill:#EDF0FD;}
.d2-1380559207 .fill-AB5{fill:#F7F8FE;}
.d2-1380559207 .stroke-N1{stroke:#0A0F25;}
.d2-1380559207 .stroke-N2{stroke:#676C7E;}
.d2-1380559207 .stroke-N3{stroke:#9499AB;}
.d2-1380559207 .stroke-N4{stroke:#CFD2DD;}
.d2-1380559207 .stroke-N5{stroke:#DEE1EB;}
.d2-1380559207 .stroke-N6{stroke:#EEF1F8;}
.d2-1380559207 .stroke-N7{stroke:#FFFFFF;}
.d2-1380559207 .stroke-B1{stroke:#0D32B2;}
.d2-1380559207 .stroke-B2{stroke:#0D32B2;}
.d2-1380559207 .stroke-B3{stroke:#E3E9FD;}
.d2-1380559207 .stroke-B4{stroke:#E3E9FD;}
.d2-1380559207 .stroke-B5{stroke:#EDF0FD;}
.d2-1380559207 .stroke-B6{stroke:#F7F8FE;}
.d2-1380559207 .stroke-AA2{stroke:#4A6FF3;}
.d2-1380559207 .stroke-AA4{stroke:#EDF0FD;}
.d2-1380559207 .stroke-AA5{stroke:#F7F8FE;}
.d2-1380559207 .stroke-AB4{stroke:#EDF0FD;}
.d2-1380559207 .stroke-AB5{stroke:#F7F8FE;}
.d2-1380559207 .background-color-N1{background-color:#0A0F25;}
.d2-1380559207 .background-color-N2{background-color:#676C7E;}
.d2-1380559207 .background-color-N3{background-color:#9499AB;}
.d2-1380559207 .background-color-N4{background-color:#CFD2DD;}
.d2-1380559207 .background-color-N5{background-color:#DEE1EB;}
.d2-1380559207 .background-color-N6{background-color:#EEF1F8;}
.d2-1380559207 .background-color-N7{background-color:#FFFFFF;}
.d2-1380559207 .background-color-B1{background-color:#0D32B2;}
.d2-1380559207 .background-color-B2{background-color:#0D32B2;}
.d2-1380559207 .background-color-B3{background-color:#E3E9FD;}
.d2-1380559207 .background-color-B4{background-color:#E3E9FD;}
.d2-1380559207 .background-color-B5{background-color:#EDF0FD;}
.d2-1380559207 .background-color-B6{background-color:#F7F8FE;}
.d2-1380559207 .background-color-AA2{background-color:#4A6FF3;}
.d2-1380559207 .background-color-AA4{background-color:#EDF0FD;}
.d2-1380559207 .background-color-AA5{background-color:#F7F8FE;}
.d2-1380559207 .background-color-AB4{background-color:#EDF0FD;}
.d2-1380559207 .background-color-AB5{background-color:#F7F8FE;}
.d2-1380559207 .color-N1{color:#0A0F25;}
.d2-1380559207 .color-N2{color:#676C7E;}
.d2-1380559207 .color-N3{color:#9499AB;}
.d2-1380559207 .color-N4{color:#CFD2DD;}
.d2-1380559207 .color-N5{color:#DEE1EB;}
.d2-1380559207 .color-N6{color:#EEF1F8;}
.d2-1380559207 .color-N7{color:#FFFFFF;}
.d2-1380559207 .color-B1{color:#0D32B2;}
.d2-1380559207 .color-B2{color:#0D32B2;}
.d2-1380559207 .color-B3{color:#E3E9FD;}
.d2-1380559207 .color-B4{color:#E3E9FD;}
.d2-1380559207 .color-B5{color:#EDF0FD;}
.d2-1380559207 .color-B6{color:#F7F8FE;}
.d2-1380559207 .color-AA2{color:#4A6FF3;}
.d2-1380559207 .color-AA4{color:#EDF0FD;}
.d2-1380559207 .color-AA5{color:#F7F8FE;}
.d2-1380559207 .color-AB4{color:#EDF0FD;}
.d2-1380559207 .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}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 27.000000 68.000000 C 27.000000 106.000000 27.000000 126.000000 27.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-1380559207)" /></g><mask id="d2-1380559207" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
<rect x="-101" y="-101" width="256" height="434" fill="white"></rect>
</mask></svg></svg>

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 469 268"><svg id="d2-svg" class="d2-1194982555" width="469" height="268" viewBox="-101 -101 469 268"><rect x="-101.000000" y="-101.000000" width="469.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 469 268"><svg id="d2-svg" class="d2-1194982555" width="469" height="268" viewBox="-101 -101 469 268"><rect x="-101.000000" y="-101.000000" width="469.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1194982555 .text-bold {
font-family: "d2-1194982555-font-bold";
}
@font-face {
font-family: d2-1194982555-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,d09GRgABAAAAAAikAAoAAAAADfwAAguFAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAGAAAABgXxHXrmNtYXAAAAFUAAAAWQAAAG4BgAJPZ2x5ZgAAAbAAAAL1AAADgDUxyYpoZWFkAAAEqAAAADYAAAA2G38e1GhoZWEAAATgAAAAJAAAACQKfwXIaG10eAAABQQAAAAkAAAAJBKMAbhsb2NhAAAFKAAAABQAAAAUBEwFGm1heHAAAAU8AAAAIAAAACAAIQD3bmFtZQAABVwAAAMoAAAIKgjwVkFwb3N0AAAIhAAAAB0AAAAg/9EAMgADAioCvAAFAAACigJYAAAASwKKAlgAAAFeADIBKQAAAgsHAwMEAwICBGAAAvcAAAADAAAAAAAAAABBREJPACAAIP//Au7/BgAAA9gBESAAAZ8AAAAAAfAClAAAACAAA3icVMw9CsJAAAbRt+76U1jsFRXERhHEoyhaKHjTLxBIIFO+YlBUBXvNEV1XcXBydnVz90hmuUySf3755pN3XnmOj2XFStWsbWztGAAAAP//AQAA//+gkRXDAAAAeJxkksFv21Qcx3/Pdp5p8JTaju2kqXFtJ35x2jSLX22PpiEJs1apS9esk+jQtlbrgQPZOqnLVDQhcekVcegOiAMn+AMQ4sAkuMIkbiDtChJ/wIQiTpmD7IwKtMv7vcN7+n6+3+8PMjAAYA6ZJ8DCHORAAgWAiqZYoYTYfEjD0NbYkCCRHzBS/PVXxOVcl6stfW48PjhA2/vMk5f3bm0fHv590GrFX37/NP4UPXwKwEBtOka/oQkUwQbQLMdfC0LHsS3MkyCgnqqINrExDr0g9DFW8uqP0eD0jLFdo1v2G8P1gw8+ynLG5hvFinxtwxD2Otdu5kxSUO7q5aPj+E+6aB9r8l52WS9okOiVp2P0B5pAAQyAjOUkgomOquQxb6oq9UINY5auJQzI2Dx+9/K91uadBsfEz7NXmn7QdPa/+JasWIHwzmj3+qjTGUZyZS6g5vsLb6F1128AALBgTesMjybQgBZspc4cfy30U71XI6CeRhU7lca2RRJ3NLGcx5j1Aj9FUPKqPLvblpM++Wt9/9KmXFoqLLjr+/6K+d0OP7d2M9QNyXIHt+9GH2/phOg6Ia7XJRVaNIVS+9eFSysbVe5C1Sh585wULW/sVIXhm1b+7a1yNqfKUusyvb6KntVc4larbi0+Kxe1eZYtFBf1xA+C3nSMJOYHyM1aEqmYV6kXJGH93G+diXMZHktCRbh1lbFfPtckhO5n+OQfAKujCZjJvlCNpmFr/1YrJh7589lLurzS9HuyudUcXD3TlyoXk6OBXnSN+nLVag7vxL8gM6hejL95NWadplnnoPRap5j8J0mkdh5E0YNO5yiKjjr11dX6ar0utB/t3hi126Mbu4/aJ9vdXr/f627DjB19hiYg/Y+dn61nClvqO8pitnChOL/YzqMXe14zk/mE41wv/h0QiNMxOmJGoKVUvm/7YUgVqtjKeXYIbu9EffHxyYmtC8WsJofCh+89u49PTx/+VKtgbogFAPgHAAD//wEAAP//TkiyNAAAAAABAAAAAguF7qAhz18PPPUAAQPoAAAAANhdoIQAAAAA3WYvNv43/sQIbQPxAAEAAwACAAAAAAAAAAEAAAPY/u8AAAiY/jf+NwhtAAEAAAAAAAAAAAAAAAAAAAAJArIAUAIPACoCBgAkAhYAIgEeAEEDWQBBAisAJAGOAEEBfwARAAAALABkAJgBAAEcAU4BegGaAcAAAQAAAAkAkAAMAGMABwABAAAAAAAAAAAAAAAAAAQAA3icnJTPbhtVFMZ/TmzTCsECRVW6ie6CRZHo2FRJ1TYrh9SKRRQHjwtCQkgTz/iPMp4ZeSYO4QlY8xa8RVc8BM+BWKP5fOzYBdEmipJ8d+75851zvnOBHf5mm0r1IfBHPTFcYa9+bniLB/UTw9u061uGqzyp/Wm4RlibG67zea1n+CPeVn8z/ID96k+GH7JbbRv+mGfVHcOfbDv+Mvwp+7xd4Aq84FfDFXbJDG+xw4+Gt3mExaxUeUTTcI3P2DNcZw/oM6EgZkLCCMeQCSOumBGR4xMxY8KQiBBHhxYxhb4mBEKO0X9+DfApmBEo4pgCR4xPTEDO2CL+Iq+Uc2Uc6jSzuxYFYwIu5HFJQIIjZURKQsSl4hQUZLyiQYOcgfhmFOR45EyI8UiZMaJBlzan9BkzIcfRVqSSmU/KkIJrAuV3ZlF2ZkBEQm6srkgIxdOJXyTvDqc4umSyXY98uhHhSxzfybvklsr2Kzz9ujVmm3mXbALm6mesrsS6udYEx7ot87b4VrjgFe5e/dlk8v4ehfpfKPIFV5p/qEklYpLg3C4tfCnId49xHOncwVdHvqdDnxO6vKGvc4sePVqc0afDa/l26eH4mi5nHMujI7y4a0sxZ/yA4xs6siljR9afxcQifiYzdefiOFMdUzL1vGTuqdZIFd59wuUOpRvqyOUz0B6Vlk7zS7RnASNTRSaGU/VyqY3c+heaIqaqpZzt7X25DXPbveUW35Bqh0u1LjiVk1swet9UvXc0c60fj4CQlAtZDEiZ0qDgRrzPCbgixnGs7p1oSwpaK58yz41UEjEVgw6J4szI9Dcw3fjGfbChe2dvSSj/kunlqqr7ZHHq1e2M3qh7yzvfuhytTaBhU03X1DQQ18S0H2mn1vn78s31uqU85YiUmPBfL8AzPJrsc8AhY2UY6GZur0NTL0STlxyq+ksiWQ2l58giHODxnAMOeMnzd/q4ZOKMi1txWc/d4pgjuhx+UBUL+y5HvF59+/+sv4tpU7U4nq5OL+49xSd3UOsX2rPb97KniZWTmFu02604I2BacnG76zW5x3j/AAAA//8BAAD///S3T1F4nGJgZgCD/+cYjBiwAAAAAAD//wEAAP//LwECAwAAAA==");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 558 268"><svg id="d2-svg" class="d2-4059799029" width="558" height="268" viewBox="-101 -101 558 268"><rect x="-101.000000" y="-101.000000" width="558.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 558 268"><svg id="d2-svg" class="d2-4059799029" width="558" height="268" viewBox="-101 -101 558 268"><rect x="-101.000000" y="-101.000000" width="558.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-4059799029 .text-bold {
font-family: "d2-4059799029-font-bold";
}
@font-face {
font-family: d2-4059799029-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 578 268"><svg id="d2-svg" class="d2-1066622782" width="578" height="268" viewBox="-101 -101 578 268"><rect x="-101.000000" y="-101.000000" width="578.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 578 268"><svg id="d2-svg" class="d2-1066622782" width="578" height="268" viewBox="-101 -101 578 268"><rect x="-101.000000" y="-101.000000" width="578.000000" height="268.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-1066622782 .text-bold {
font-family: "d2-1066622782-font-bold";
}
@font-face {
font-family: d2-1066622782-font-bold;
src: url("data:application/font-woff;base64,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");
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.1-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-2065426295" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2065426295 .text-bold {
font-family: "d2-2065426295-font-bold";
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.4.2-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-2561523587" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2561523587 .text-bold {
font-family: "d2-2561523587-font-bold";
}
@font-face {
font-family: d2-2065426295-font-bold;
src: url("data:application/font-woff;base64,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");
font-family: d2-2561523587-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
@ -18,78 +18,78 @@
opacity: 0.5;
}
.d2-2065426295 .fill-N1{fill:#0A0F25;}
.d2-2065426295 .fill-N2{fill:#676C7E;}
.d2-2065426295 .fill-N3{fill:#9499AB;}
.d2-2065426295 .fill-N4{fill:#CFD2DD;}
.d2-2065426295 .fill-N5{fill:#DEE1EB;}
.d2-2065426295 .fill-N6{fill:#EEF1F8;}
.d2-2065426295 .fill-N7{fill:#FFFFFF;}
.d2-2065426295 .fill-B1{fill:#0D32B2;}
.d2-2065426295 .fill-B2{fill:#0D32B2;}
.d2-2065426295 .fill-B3{fill:#E3E9FD;}
.d2-2065426295 .fill-B4{fill:#E3E9FD;}
.d2-2065426295 .fill-B5{fill:#EDF0FD;}
.d2-2065426295 .fill-B6{fill:#F7F8FE;}
.d2-2065426295 .fill-AA2{fill:#4A6FF3;}
.d2-2065426295 .fill-AA4{fill:#EDF0FD;}
.d2-2065426295 .fill-AA5{fill:#F7F8FE;}
.d2-2065426295 .fill-AB4{fill:#EDF0FD;}
.d2-2065426295 .fill-AB5{fill:#F7F8FE;}
.d2-2065426295 .stroke-N1{stroke:#0A0F25;}
.d2-2065426295 .stroke-N2{stroke:#676C7E;}
.d2-2065426295 .stroke-N3{stroke:#9499AB;}
.d2-2065426295 .stroke-N4{stroke:#CFD2DD;}
.d2-2065426295 .stroke-N5{stroke:#DEE1EB;}
.d2-2065426295 .stroke-N6{stroke:#EEF1F8;}
.d2-2065426295 .stroke-N7{stroke:#FFFFFF;}
.d2-2065426295 .stroke-B1{stroke:#0D32B2;}
.d2-2065426295 .stroke-B2{stroke:#0D32B2;}
.d2-2065426295 .stroke-B3{stroke:#E3E9FD;}
.d2-2065426295 .stroke-B4{stroke:#E3E9FD;}
.d2-2065426295 .stroke-B5{stroke:#EDF0FD;}
.d2-2065426295 .stroke-B6{stroke:#F7F8FE;}
.d2-2065426295 .stroke-AA2{stroke:#4A6FF3;}
.d2-2065426295 .stroke-AA4{stroke:#EDF0FD;}
.d2-2065426295 .stroke-AA5{stroke:#F7F8FE;}
.d2-2065426295 .stroke-AB4{stroke:#EDF0FD;}
.d2-2065426295 .stroke-AB5{stroke:#F7F8FE;}
.d2-2065426295 .background-color-N1{background-color:#0A0F25;}
.d2-2065426295 .background-color-N2{background-color:#676C7E;}
.d2-2065426295 .background-color-N3{background-color:#9499AB;}
.d2-2065426295 .background-color-N4{background-color:#CFD2DD;}
.d2-2065426295 .background-color-N5{background-color:#DEE1EB;}
.d2-2065426295 .background-color-N6{background-color:#EEF1F8;}
.d2-2065426295 .background-color-N7{background-color:#FFFFFF;}
.d2-2065426295 .background-color-B1{background-color:#0D32B2;}
.d2-2065426295 .background-color-B2{background-color:#0D32B2;}
.d2-2065426295 .background-color-B3{background-color:#E3E9FD;}
.d2-2065426295 .background-color-B4{background-color:#E3E9FD;}
.d2-2065426295 .background-color-B5{background-color:#EDF0FD;}
.d2-2065426295 .background-color-B6{background-color:#F7F8FE;}
.d2-2065426295 .background-color-AA2{background-color:#4A6FF3;}
.d2-2065426295 .background-color-AA4{background-color:#EDF0FD;}
.d2-2065426295 .background-color-AA5{background-color:#F7F8FE;}
.d2-2065426295 .background-color-AB4{background-color:#EDF0FD;}
.d2-2065426295 .background-color-AB5{background-color:#F7F8FE;}
.d2-2065426295 .color-N1{color:#0A0F25;}
.d2-2065426295 .color-N2{color:#676C7E;}
.d2-2065426295 .color-N3{color:#9499AB;}
.d2-2065426295 .color-N4{color:#CFD2DD;}
.d2-2065426295 .color-N5{color:#DEE1EB;}
.d2-2065426295 .color-N6{color:#EEF1F8;}
.d2-2065426295 .color-N7{color:#FFFFFF;}
.d2-2065426295 .color-B1{color:#0D32B2;}
.d2-2065426295 .color-B2{color:#0D32B2;}
.d2-2065426295 .color-B3{color:#E3E9FD;}
.d2-2065426295 .color-B4{color:#E3E9FD;}
.d2-2065426295 .color-B5{color:#EDF0FD;}
.d2-2065426295 .color-B6{color:#F7F8FE;}
.d2-2065426295 .color-AA2{color:#4A6FF3;}
.d2-2065426295 .color-AA4{color:#EDF0FD;}
.d2-2065426295 .color-AA5{color:#F7F8FE;}
.d2-2065426295 .color-AB4{color:#EDF0FD;}
.d2-2065426295 .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}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 22.784863 67.985640 C 18.204819 106.000000 18.200000 126.000000 22.523419 162.028493" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2065426295)" /></g><g id="(y -&gt; x)[0]"><path d="M 31.215137 164.014360 C 35.795181 126.000000 35.800000 106.000000 31.476581 69.971507" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2065426295)" /></g><mask id="d2-2065426295" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
.d2-2561523587 .fill-N1{fill:#0A0F25;}
.d2-2561523587 .fill-N2{fill:#676C7E;}
.d2-2561523587 .fill-N3{fill:#9499AB;}
.d2-2561523587 .fill-N4{fill:#CFD2DD;}
.d2-2561523587 .fill-N5{fill:#DEE1EB;}
.d2-2561523587 .fill-N6{fill:#EEF1F8;}
.d2-2561523587 .fill-N7{fill:#FFFFFF;}
.d2-2561523587 .fill-B1{fill:#0D32B2;}
.d2-2561523587 .fill-B2{fill:#0D32B2;}
.d2-2561523587 .fill-B3{fill:#E3E9FD;}
.d2-2561523587 .fill-B4{fill:#E3E9FD;}
.d2-2561523587 .fill-B5{fill:#EDF0FD;}
.d2-2561523587 .fill-B6{fill:#F7F8FE;}
.d2-2561523587 .fill-AA2{fill:#4A6FF3;}
.d2-2561523587 .fill-AA4{fill:#EDF0FD;}
.d2-2561523587 .fill-AA5{fill:#F7F8FE;}
.d2-2561523587 .fill-AB4{fill:#EDF0FD;}
.d2-2561523587 .fill-AB5{fill:#F7F8FE;}
.d2-2561523587 .stroke-N1{stroke:#0A0F25;}
.d2-2561523587 .stroke-N2{stroke:#676C7E;}
.d2-2561523587 .stroke-N3{stroke:#9499AB;}
.d2-2561523587 .stroke-N4{stroke:#CFD2DD;}
.d2-2561523587 .stroke-N5{stroke:#DEE1EB;}
.d2-2561523587 .stroke-N6{stroke:#EEF1F8;}
.d2-2561523587 .stroke-N7{stroke:#FFFFFF;}
.d2-2561523587 .stroke-B1{stroke:#0D32B2;}
.d2-2561523587 .stroke-B2{stroke:#0D32B2;}
.d2-2561523587 .stroke-B3{stroke:#E3E9FD;}
.d2-2561523587 .stroke-B4{stroke:#E3E9FD;}
.d2-2561523587 .stroke-B5{stroke:#EDF0FD;}
.d2-2561523587 .stroke-B6{stroke:#F7F8FE;}
.d2-2561523587 .stroke-AA2{stroke:#4A6FF3;}
.d2-2561523587 .stroke-AA4{stroke:#EDF0FD;}
.d2-2561523587 .stroke-AA5{stroke:#F7F8FE;}
.d2-2561523587 .stroke-AB4{stroke:#EDF0FD;}
.d2-2561523587 .stroke-AB5{stroke:#F7F8FE;}
.d2-2561523587 .background-color-N1{background-color:#0A0F25;}
.d2-2561523587 .background-color-N2{background-color:#676C7E;}
.d2-2561523587 .background-color-N3{background-color:#9499AB;}
.d2-2561523587 .background-color-N4{background-color:#CFD2DD;}
.d2-2561523587 .background-color-N5{background-color:#DEE1EB;}
.d2-2561523587 .background-color-N6{background-color:#EEF1F8;}
.d2-2561523587 .background-color-N7{background-color:#FFFFFF;}
.d2-2561523587 .background-color-B1{background-color:#0D32B2;}
.d2-2561523587 .background-color-B2{background-color:#0D32B2;}
.d2-2561523587 .background-color-B3{background-color:#E3E9FD;}
.d2-2561523587 .background-color-B4{background-color:#E3E9FD;}
.d2-2561523587 .background-color-B5{background-color:#EDF0FD;}
.d2-2561523587 .background-color-B6{background-color:#F7F8FE;}
.d2-2561523587 .background-color-AA2{background-color:#4A6FF3;}
.d2-2561523587 .background-color-AA4{background-color:#EDF0FD;}
.d2-2561523587 .background-color-AA5{background-color:#F7F8FE;}
.d2-2561523587 .background-color-AB4{background-color:#EDF0FD;}
.d2-2561523587 .background-color-AB5{background-color:#F7F8FE;}
.d2-2561523587 .color-N1{color:#0A0F25;}
.d2-2561523587 .color-N2{color:#676C7E;}
.d2-2561523587 .color-N3{color:#9499AB;}
.d2-2561523587 .color-N4{color:#CFD2DD;}
.d2-2561523587 .color-N5{color:#DEE1EB;}
.d2-2561523587 .color-N6{color:#EEF1F8;}
.d2-2561523587 .color-N7{color:#FFFFFF;}
.d2-2561523587 .color-B1{color:#0D32B2;}
.d2-2561523587 .color-B2{color:#0D32B2;}
.d2-2561523587 .color-B3{color:#E3E9FD;}
.d2-2561523587 .color-B4{color:#E3E9FD;}
.d2-2561523587 .color-B5{color:#EDF0FD;}
.d2-2561523587 .color-B6{color:#F7F8FE;}
.d2-2561523587 .color-AA2{color:#4A6FF3;}
.d2-2561523587 .color-AA4{color:#EDF0FD;}
.d2-2561523587 .color-AA5{color:#F7F8FE;}
.d2-2561523587 .color-AB4{color:#EDF0FD;}
.d2-2561523587 .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}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 22.784731 67.985636 C 18.204000 106.000000 18.200001 126.000000 22.523419 162.028493" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2561523587)" /></g><g id="(y -&gt; x)[0]"><path d="M 31.214269 164.014364 C 35.794998 126.000000 35.799999 106.000000 31.476581 69.971507" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2561523587)" /></g><mask id="d2-2561523587" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
<rect x="-101" y="-101" width="256" height="434" fill="white"></rect>
</mask></svg></svg>

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Some files were not shown because too many files have changed in this diff Show more