Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ D2 Third-Party Notices
Dagro
https://github.com/d2lang/dagro

Dagro is a Go port of Dagre 0.8.5 and the subset of Graphlib 2.1.8 required by
Dagre's layout implementation. The port is based on these upstream sources:
Dagro is a Go port of the Dagre 3.1.1 layout surface used by D2 and the subset
of Graphlib 4.0.5 required by that layout implementation. The port is based on
these upstream sources:

Dagre 0.8.5
Dagre 3.1.1
https://github.com/dagrejs/dagre
commit f56edb1abbb8530e532158f7cbd403228f5b0018
commit c3ed0802cd98de74c21cff1f754689ebbb0f8dae

Graphlib 2.1.8
Graphlib 4.0.5
https://github.com/dagrejs/graphlib
commit 64375bb8d96bce0d906d238853c2b5afa2f2c231
commit d3a0cf36f55ebd75f28b6acf7a436a54e1b990dc

MIT License

Expand Down
3 changes: 2 additions & 1 deletion ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- api: deprecate legacy layout-feature constants, raw-WASM ELK/object-order bridges, unused public wrappers, and test-only comparison, validation, and logging helpers; compatibility entry points remain callable for one release while in-repository callers use supported or internal replacements
- maintenance: update the Go toolchain to 1.26.5 and refresh Go, d2.js, CI, and release dependencies
- renders: update syntax highlighting and migrate archived font and PDF dependencies to maintained replacements
- d2dagre: replace the embedded JavaScript runtime with the native Go Dagro port without changing layout output
- d2dagre: replace the embedded JavaScript runtime with native Go Dagro and update its D2-used layout surface from Dagre 0.8.5 to Dagre 3.1.1 behavior. This intentionally changes node ordering and coordinates, edge routes, self-loops, and compound sizing in some diagrams; regenerate and review stored SVG or board-JSON snapshots
- d2sketch: replace the embedded Rough.js runtime with the native Go rough-go port without changing sketch output
- d2elk: replace the embedded ELK.js 0.8.2 runtime with native Go elk-go and update D2's ELK layout profile to ELK.js 0.12.0 behavior. This is a layout-behavior update, not an output-compatible runtime swap:
- existing ELK diagrams may receive different node coordinates and ordering, edge and label routes, and component packing; regenerate and review stored SVG or board-JSON snapshots
Expand Down Expand Up @@ -40,6 +40,7 @@

#### Bugfixes ⛑️

- d2dagre: keep same-direction parallel edges and self-loop routes finite when using Dagre 3.1.1 behavior
- d2svg: reject padding that would produce invalid negative SVG dimensions
- d2elk: route ancestor-to-descendant connections around intermediate containers
- d2elk: prevent labels on multiple self-loops from overlapping in right-directed layouts, including the `ent2d2_right` regression case
Expand Down
13 changes: 7 additions & 6 deletions d2js/js/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ D2 Third-Party Notices
Dagro
https://github.com/d2lang/dagro

Dagro is a Go port of Dagre 0.8.5 and the subset of Graphlib 2.1.8 required by
Dagre's layout implementation. The port is based on these upstream sources:
Dagro is a Go port of the Dagre 3.1.1 layout surface used by D2 and the subset
of Graphlib 4.0.5 required by that layout implementation. The port is based on
these upstream sources:

Dagre 0.8.5
Dagre 3.1.1
https://github.com/dagrejs/dagre
commit f56edb1abbb8530e532158f7cbd403228f5b0018
commit c3ed0802cd98de74c21cff1f754689ebbb0f8dae

Graphlib 2.1.8
Graphlib 4.0.5
https://github.com/dagrejs/graphlib
commit 64375bb8d96bce0d906d238853c2b5afa2f2c231
commit d3a0cf36f55ebd75f28b6acf7a436a54e1b990dc

MIT License

Expand Down
3 changes: 2 additions & 1 deletion d2layouts/d2dagrelayout/NOTICE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
D2's Dagre layout uses Dagro, a native Go port of Dagre 0.8.5:
D2's Dagre layout uses Dagro, a native Go port of the Dagre 3.1.1 layout
surface used by D2:
https://github.com/d2lang/dagro

The complete Dagro, Dagre, and Graphlib attribution and MIT license text is in
Expand Down
28 changes: 28 additions & 0 deletions d2layouts/d2dagrelayout/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
points[i] = p
}
}
// Dagre 3.1 self-loop routes contain repeated control points. Remove only
// consecutive duplicates so downstream tangent calculations never see a
// zero-length segment.
points = deduplicateRoutePoints(points)
if edge.Src == edge.Dst {
// The self-loop controls describe the loop outside the node, but do not
// include usable node-facing endpoints. Center sentinels give
// TraceToShape a direction from which to chop the route to each border.
center := edge.Src.Center()
points = append([]*geo.Point{center.Copy()}, points...)
points = append(points, center.Copy())
}
if len(points) < 2 {
return fmt.Errorf("dagro returned edge %q without a usable route", dagreEdges[i].Name)
}

startIndex, endIndex := 0, len(points)-1
start, end := points[startIndex], points[endIndex]
Expand Down Expand Up @@ -370,6 +385,19 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
return nil
}

func deduplicateRoutePoints(points []*geo.Point) []*geo.Point {
if len(points) < 2 {
return points
}
deduplicated := points[:1]
for _, point := range points[1:] {
if !point.Equals(deduplicated[len(deduplicated)-1]) {
deduplicated = append(deduplicated, point)
}
}
return deduplicated
}

type containerEndpoints struct {
head, tail *d2graph.Object
}
Expand Down
120 changes: 120 additions & 0 deletions d2layouts/d2dagrelayout/layout_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,135 @@
package d2dagrelayout

import (
"context"
"math"
"strings"
"testing"

"github.com/d2lang/d2/d2compiler"
"github.com/d2lang/d2/d2graph"
"github.com/d2lang/d2/d2themes/d2themescatalog"
"github.com/d2lang/d2/lib/geo"
"github.com/d2lang/d2/lib/textmeasure"
"github.com/d2lang/util-go/go2"
)

func TestDeduplicateRoutePoints(t *testing.T) {
t.Parallel()
points := []*geo.Point{
geo.NewPoint(1, 2),
geo.NewPoint(1, 2),
geo.NewPoint(3, 4),
geo.NewPoint(3, 4),
geo.NewPoint(1, 2),
}
got := deduplicateRoutePoints(points)
want := []*geo.Point{geo.NewPoint(1, 2), geo.NewPoint(3, 4), geo.NewPoint(1, 2)}
if len(got) != len(want) {
t.Fatalf("deduplicated route length = %d, want %d", len(got), len(want))
}
for i := range want {
if !got[i].Equals(want[i]) {
t.Fatalf("deduplicated route[%d] = %v, want %v", i, got[i], want[i])
}
}
}

func TestModernSelfLoopRoutesRemainFiniteAndConnected(t *testing.T) {
t.Parallel()
g, _, err := d2compiler.Compile("index.d2", strings.NewReader("x -> x\nx -> x"), nil)
if err != nil {
t.Fatal(err)
}
if err := g.ApplyTheme(d2themescatalog.NeutralDefault.ID); err != nil {
t.Fatal(err)
}
ruler, err := textmeasure.NewRuler()
if err != nil {
t.Fatal(err)
}
if err := g.SetDimensions(nil, ruler, nil, nil); err != nil {
t.Fatal(err)
}
if err := DefaultLayout(context.Background(), g); err != nil {
t.Fatal(err)
}
for _, edge := range g.Edges {
if len(edge.Route) < 2 {
t.Fatalf("%s route has %d points", edge.AbsID(), len(edge.Route))
}
for i, point := range edge.Route {
if math.IsNaN(point.X) || math.IsNaN(point.Y) || math.IsInf(point.X, 0) || math.IsInf(point.Y, 0) {
t.Fatalf("%s route[%d] is not finite: %v", edge.AbsID(), i, point)
}
}
if !pointOnBoxBorder(edge.Route[0], edge.Src.Box) {
t.Fatalf("%s route does not start on its source: %v, box %v", edge.AbsID(), edge.Route[0], edge.Src.Box)
}
if !pointOnBoxBorder(edge.Route[len(edge.Route)-1], edge.Dst.Box) {
t.Fatalf("%s route does not end on its destination: %v, box %v", edge.AbsID(), edge.Route[len(edge.Route)-1], edge.Dst.Box)
}
}
}

func TestCompoundParallelCycleRemainsFinite(t *testing.T) {
t.Parallel()
g, _, err := d2compiler.Compile("index.d2", strings.NewReader("a.b -> c -> a.b <- c"), nil)
if err != nil {
t.Fatal(err)
}
if err := g.ApplyTheme(d2themescatalog.NeutralDefault.ID); err != nil {
t.Fatal(err)
}
ruler, err := textmeasure.NewRuler()
if err != nil {
t.Fatal(err)
}
if err := g.SetDimensions(nil, ruler, nil, nil); err != nil {
t.Fatal(err)
}
if err := DefaultLayout(context.Background(), g); err != nil {
t.Fatal(err)
}
if got, want := len(g.Edges), 3; got != want {
t.Fatalf("edges = %d, want %d", got, want)
}
for _, obj := range g.Objects {
for name, value := range map[string]float64{
"x": obj.TopLeft.X, "y": obj.TopLeft.Y, "width": obj.Width, "height": obj.Height,
} {
if math.IsNaN(value) || math.IsInf(value, 0) {
t.Fatalf("%s %s is not finite: %v", obj.AbsID(), name, value)
}
}
}
for _, edge := range g.Edges {
if len(edge.Route) < 2 {
t.Fatalf("%s route has %d points", edge.AbsID(), len(edge.Route))
}
for i, point := range edge.Route {
if math.IsNaN(point.X) || math.IsNaN(point.Y) || math.IsInf(point.X, 0) || math.IsInf(point.Y, 0) {
t.Fatalf("%s route[%d] is not finite: %v", edge.AbsID(), i, point)
}
}
if !pointOnBoxBorder(edge.Route[0], edge.Src.Box) {
t.Fatalf("%s route does not start on its source: %v, box %v", edge.AbsID(), edge.Route[0], edge.Src.Box)
}
if !pointOnBoxBorder(edge.Route[len(edge.Route)-1], edge.Dst.Box) {
t.Fatalf("%s route does not end on its destination: %v, box %v", edge.AbsID(), edge.Route[len(edge.Route)-1], edge.Dst.Box)
}
}
}

func pointOnBoxBorder(point *geo.Point, box *geo.Box) bool {
const epsilon = 1
left, right := box.TopLeft.X, box.TopLeft.X+box.Width
top, bottom := box.TopLeft.Y, box.TopLeft.Y+box.Height
near := func(a, b float64) bool { return math.Abs(a-b) <= epsilon }
return ((near(point.X, left) || near(point.X, right)) && point.Y >= top-epsilon && point.Y <= bottom+epsilon) ||
((near(point.Y, top) || near(point.Y, bottom)) && point.X >= left-epsilon && point.X <= right+epsilon)
}

func TestContainerTopologyMatchesLegacyTraversal(t *testing.T) {
t.Parallel()
tests := map[string]string{
Expand Down
2 changes: 1 addition & 1 deletion d2plugin/plugin_dagre.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (p *dagrePlugin) Info(ctx context.Context) (*PluginInfo, error) {
LongHelp: fmt.Sprintf(`dagre is a directed graph layout algorithm implemented natively in Go by Dagro.
See https://d2lang.com/tour/dagre for more.

Dagro is a behavior-compatible port of Dagre 0.8.5: https://github.com/d2lang/dagro.
Dagro implements the Dagre 3.1.1 layout surface used by D2: https://github.com/d2lang/dagro.

Flags correspond to ones found at https://github.com/dagrejs/dagre/wiki.

Expand Down
168 changes: 84 additions & 84 deletions d2renderers/d2sketch/testdata/animated/sketch.exp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 83 additions & 83 deletions d2renderers/d2sketch/testdata/animated_dark/sketch.exp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading