diff --git a/d2exporter/export.go b/d2exporter/export.go index 8f9331263..3a1274f89 100644 --- a/d2exporter/export.go +++ b/d2exporter/export.go @@ -8,7 +8,6 @@ import ( "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/d2themes/d2themescatalog" - "oss.terrastruct.com/d2/lib/go2" ) func Export(ctx context.Context, g *d2graph.Graph, themeID int64) (*d2target.Diagram, error) { @@ -17,16 +16,13 @@ func Export(ctx context.Context, g *d2graph.Graph, themeID int64) (*d2target.Dia diagram := d2target.NewDiagram() diagram.Shapes = make([]d2target.Shape, len(g.Objects)) - maxObjectZIndex := 0 for i := range g.Objects { diagram.Shapes[i] = toShape(g.Objects[i], &theme) - maxObjectZIndex = go2.IntMax(maxObjectZIndex, diagram.Shapes[i].ZIndex) } - edgeDefaultZIndex := maxObjectZIndex + 1 diagram.Connections = make([]d2target.Connection, len(g.Edges)) for i := range g.Edges { - diagram.Connections[i] = toConnection(g.Edges[i], &theme, edgeDefaultZIndex) + diagram.Connections[i] = toConnection(g.Edges[i], &theme) } return diagram, nil @@ -92,11 +88,8 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { shape := d2target.BaseShape() shape.SetType(obj.Attributes.Shape.Value) shape.ID = obj.AbsID() - if obj.ZIndex == nil { - shape.ZIndex = int(obj.Level()) - } else { - shape.ZIndex = *obj.ZIndex - } + shape.ZIndex = obj.ZIndex + shape.Level = int(obj.Level()) shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y)) shape.Width = int(obj.Width) shape.Height = int(obj.Height) @@ -138,14 +131,10 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { return *shape } -func toConnection(edge *d2graph.Edge, theme *d2themes.Theme, defaultZIndex int) d2target.Connection { +func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection { connection := d2target.BaseConnection() connection.ID = edge.AbsID() - if edge.ZIndex == nil { - connection.ZIndex = defaultZIndex - } else { - connection.ZIndex = *edge.ZIndex - } + connection.ZIndex = edge.ZIndex // edge.Edge.ID = go2.StringToIntHash(connection.ID) text := edge.Text() diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index 5a00b2b75..c8f50330b 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -79,7 +79,7 @@ type Object struct { Attributes Attributes `json:"attributes"` - ZIndex *int `json:"zIndex,omitempty"` + ZIndex int `json:"zIndex"` } type Attributes struct { @@ -633,7 +633,7 @@ type Edge struct { References []EdgeReference `json:"references,omitempty"` Attributes Attributes `json:"attributes"` - ZIndex *int `json:"zIndex,omitempty"` + ZIndex int `json:"zIndex"` } type EdgeReference struct { diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 84bbd2f15..c81a75f81 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -993,9 +993,7 @@ func Render(diagram *d2target.Diagram) ([]byte, error) { allObjects = append(allObjects, c) } - sort.SliceStable(allObjects, func(i, j int) bool { - return allObjects[i].GetZIndex() < allObjects[j].GetZIndex() - }) + sortObjects(allObjects) markers := map[string]struct{}{} for _, obj := range allObjects { @@ -1017,6 +1015,34 @@ func Render(diagram *d2target.Diagram) ([]byte, error) { return buf.Bytes(), nil } +// sortObjects sorts all diagrams objects (shapes and connections) in the desired drawing order +// the sorting criteria is: +// 1. zIndex, lower comes first +// 2. two shapes with the same zIndex are sorted by their level (container nesting), containers come first +// 3. two shapes with the same zIndex and same level, are sorted in the order they were exported +// 4. shape and edge, shapes come first +func sortObjects(allObjects []d2target.DiagramObject) { + sort.SliceStable(allObjects, func(i, j int) bool { + // first sort by zIndex + iZIndex := allObjects[i].GetZIndex() + jZIndex := allObjects[j].GetZIndex() + if iZIndex != jZIndex { + return iZIndex < jZIndex + } + + // then, if both are shapes, the containers come first + iShape, iIsShape := allObjects[i].(d2target.Shape) + jShape, jIsShape := allObjects[j].(d2target.Shape) + if iIsShape && jIsShape { + return iShape.Level < jShape.Level + } + + // then, shapes come before connections + _, jIsConnection := allObjects[j].(d2target.Connection) + return iIsShape && jIsConnection + }) +} + func hash(s string) string { const secret = "lalalas" h := fnv.New32a() diff --git a/d2renderers/d2svg/d2svg_test.go b/d2renderers/d2svg/d2svg_test.go new file mode 100644 index 000000000..8e6ed0277 --- /dev/null +++ b/d2renderers/d2svg/d2svg_test.go @@ -0,0 +1,79 @@ +package d2svg + +import ( + "testing" + + "oss.terrastruct.com/d2/d2target" +) + +func TestSortObjects(t *testing.T) { + allObjects := []d2target.DiagramObject{ + // same zIndex and level, should keep in this order + d2target.Shape{ + ID: "0", + ZIndex: 0, + Level: 0, + }, + d2target.Shape{ + ID: "1", + ZIndex: 0, + Level: 0, + }, + // same zIndex, different level, should be swapped + d2target.Shape{ + ID: "2", + ZIndex: 0, + Level: 1, + }, + d2target.Shape{ + ID: "3", + ZIndex: 0, + Level: 0, + }, + // different zIndex, should come after connections + d2target.Shape{ + ID: "4", + ZIndex: 1, + Level: 0, + }, + // connections come after shapes + d2target.Connection{ + ID: "5", + ZIndex: 0, + }, + d2target.Connection{ + ID: "6", + ZIndex: 0, + }, + // this should be last object + d2target.Connection{ + ID: "7", + ZIndex: 2, + }, + // this should be the first object + d2target.Connection{ + ID: "8", + ZIndex: -1, + }, + } + + expectedOrder := []d2target.DiagramObject{ + allObjects[8], + allObjects[0], + allObjects[1], + allObjects[3], + allObjects[2], + allObjects[5], + allObjects[6], + allObjects[4], + allObjects[7], + } + + sortObjects(allObjects) + + for i := 0; i < len(allObjects); i++ { + if allObjects[i].GetID() != expectedOrder[i].GetID() { + t.Fatalf("object order differs at index %d, got '%s' expected '%s'", i, allObjects[i].GetID(), expectedOrder[i].GetID()) + } + } +} diff --git a/d2target/d2target.go b/d2target/d2target.go index 2a50a6aa4..dd386c72d 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -18,6 +18,7 @@ const ( ) type DiagramObject interface { + GetID() string GetZIndex() int } @@ -122,6 +123,7 @@ type Shape struct { LabelPosition string `json:"labelPosition,omitempty"` ZIndex int `json:"zIndex"` + Level int `json:"level"` } func (s *Shape) SetType(t string) { @@ -139,6 +141,10 @@ func (s Shape) GetZIndex() int { return s.ZIndex } +func (s Shape) GetID() string { + return s.ID +} + type Text struct { Label string `json:"label"` FontSize int `json:"fontSize"` @@ -225,6 +231,10 @@ func (c Connection) GetZIndex() int { return c.ZIndex } +func (c Connection) GetID() string { + return c.ID +} + type Arrowhead string const (