diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index f3c0d2a77..f8c1119ab 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -2,4 +2,6 @@
#### Improvements 🧹
+- Use shape specific sizing for grid containers [#1294](https://github.com/terrastruct/d2/pull/1294)
+
#### Bugfixes ⛑️
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 1bab9c273..aa211bb7e 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -1052,6 +1052,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 {
@@ -1435,7 +1474,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.
@@ -1468,27 +1506,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
diff --git a/d2layouts/d2grid/layout.go b/d2layouts/d2grid/layout.go
index dd87f4cb6..d46475518 100644
--- a/d2layouts/d2grid/layout.go
+++ b/d2layouts/d2grid/layout.go
@@ -5,10 +5,13 @@ import (
"fmt"
"math"
"sort"
+ "strings"
"oss.terrastruct.com/d2/d2graph"
+ "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
+ "oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/util-go/go2"
)
@@ -70,26 +73,37 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams ma
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
+ if obj.Box != nil {
+ // size shape according to grid
+ obj.SizeToContent(float64(gd.width), float64(gd.height), 2*CONTAINER_PADDING, 2*CONTAINER_PADDING)
+
+ // compute where the grid should be placed inside shape
+ dslShape := strings.ToLower(obj.Shape.Value)
+ shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
+ s := shape.NewShape(shapeType, geo.NewBox(geo.NewPoint(0, 0), obj.Width, obj.Height))
+ innerBox := s.GetInnerBox()
+ if innerBox.TopLeft.X != 0 || innerBox.TopLeft.Y != 0 {
+ gd.shift(innerBox.TopLeft.X, innerBox.TopLeft.Y)
+ }
+
+ var dx, dy float64
+ labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
+ if labelWidth > obj.Width {
+ dx = (labelWidth - obj.Width) / 2
+ obj.Width = labelWidth
+ }
+ 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
+ obj.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)
+ }
}
- 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
diff --git a/e2etests/regression_test.go b/e2etests/regression_test.go
index a2d16828f..e0453a15e 100644
--- a/e2etests/regression_test.go
+++ b/e2etests/regression_test.go
@@ -946,6 +946,7 @@ a -> b -> c
},
loadFromFile(t, "slow_grid"),
loadFromFile(t, "grid_oom"),
+ loadFromFile(t, "cylinder_grid_label"),
}
runa(t, tcs)
diff --git a/e2etests/testdata/files/cylinder_grid_label.d2 b/e2etests/testdata/files/cylinder_grid_label.d2
new file mode 100644
index 000000000..a5048b659
--- /dev/null
+++ b/e2etests/testdata/files/cylinder_grid_label.d2
@@ -0,0 +1,6 @@
+container title is hidden: {
+ shape: cylinder
+ grid-columns: 1
+ first
+ second
+}
diff --git a/e2etests/testdata/regression/cylinder_grid_label/dagre/board.exp.json b/e2etests/testdata/regression/cylinder_grid_label/dagre/board.exp.json
new file mode 100644
index 000000000..d51e98a67
--- /dev/null
+++ b/e2etests/testdata/regression/cylinder_grid_label/dagre/board.exp.json
@@ -0,0 +1,171 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "container title is hidden",
+ "type": "cylinder",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 286,
+ "height": 364,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "container title is hidden",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 276,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "container title is hidden.first",
+ "type": "rectangle",
+ "pos": {
+ "x": 95,
+ "y": 108
+ },
+ "width": 95,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "first",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 30,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "container title is hidden.second",
+ "type": "rectangle",
+ "pos": {
+ "x": 95,
+ "y": 214
+ },
+ "width": 95,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "second",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/regression/cylinder_grid_label/dagre/sketch.exp.svg b/e2etests/testdata/regression/cylinder_grid_label/dagre/sketch.exp.svg
new file mode 100644
index 000000000..2d1f01190
--- /dev/null
+++ b/e2etests/testdata/regression/cylinder_grid_label/dagre/sketch.exp.svg
@@ -0,0 +1,102 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/cylinder_grid_label/elk/board.exp.json b/e2etests/testdata/regression/cylinder_grid_label/elk/board.exp.json
new file mode 100644
index 000000000..7539ef058
--- /dev/null
+++ b/e2etests/testdata/regression/cylinder_grid_label/elk/board.exp.json
@@ -0,0 +1,171 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "container title is hidden",
+ "type": "cylinder",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 286,
+ "height": 364,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "AA4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "container title is hidden",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 276,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "container title is hidden.first",
+ "type": "rectangle",
+ "pos": {
+ "x": 107,
+ "y": 120
+ },
+ "width": 95,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "first",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 30,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "container title is hidden.second",
+ "type": "rectangle",
+ "pos": {
+ "x": 107,
+ "y": 226
+ },
+ "width": 95,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "second",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/regression/cylinder_grid_label/elk/sketch.exp.svg b/e2etests/testdata/regression/cylinder_grid_label/elk/sketch.exp.svg
new file mode 100644
index 000000000..371c838d9
--- /dev/null
+++ b/e2etests/testdata/regression/cylinder_grid_label/elk/sketch.exp.svg
@@ -0,0 +1,102 @@
+container title is hiddenfirstsecond
+
+
+
\ No newline at end of file