diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 406e245d5..22e973a91 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -11,6 +11,7 @@ - All vars defined in a scope are accessible everywhere in that scope, i.e., an object can use a var defined after itself. [#1695](https://github.com/terrastruct/d2/pull/1695) - Encoding API switches to standard zlib encoding so that decoding doesn't depend on source. [#1709](https://github.com/terrastruct/d2/pull/1709) - `currentcolor` is accepted as a color option to inherit parent colors. (ty @hboomsma) [#1700](https://github.com/terrastruct/d2/pull/1700) +- grid containers can now be sized with `width`/`height` even when using a layout plugin without that feature. [#1731](https://github.com/terrastruct/d2/pull/1731) #### Bugfixes ⛑️ diff --git a/d2layouts/d2grid/layout.go b/d2layouts/d2grid/layout.go index f0956d053..fc9b4a91e 100644 --- a/d2layouts/d2grid/layout.go +++ b/d2layouts/d2grid/layout.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "math" + "strconv" "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2target" @@ -47,15 +48,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error { 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) - } + contentWidth, contentHeight := gd.width, gd.height var labelPosition, iconPosition label.Position if obj.LabelPosition != nil { @@ -83,7 +76,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error { label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight, label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight, label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight: - overflow := labelWidth - (obj.Width - float64(2*horizontalPadding)) + overflow := labelWidth - contentWidth if overflow > 0 { padding.Left += overflow / 2 padding.Right += overflow / 2 @@ -95,7 +88,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error { case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom, label.InsideMiddleLeft, label.InsideMiddleCenter, label.InsideMiddleRight, label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom: - overflow := labelHeight - (obj.Height - float64(2*verticalPadding)) + overflow := labelHeight - contentHeight if overflow > 0 { padding.Top += overflow / 2 padding.Bottom += overflow / 2 @@ -112,7 +105,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error { padding.Left = math.Max(padding.Left, iconSize) padding.Right = math.Max(padding.Right, iconSize) minWidth := 2*iconSize + float64(obj.LabelDimensions.Width) + 2*label.PADDING - overflow := minWidth - (obj.Width - float64(2*horizontalPadding)) + overflow := minWidth - contentWidth if overflow > 0 { padding.Left = math.Max(padding.Left, overflow/2) padding.Right = math.Max(padding.Right, overflow/2) @@ -121,24 +114,65 @@ func Layout(ctx context.Context, g *d2graph.Graph) error { overflowTop := padding.Top - float64(verticalPadding) if overflowTop > 0 { - obj.Height += overflowTop + contentHeight += overflowTop dy += overflowTop } overflowBottom := padding.Bottom - float64(verticalPadding) if overflowBottom > 0 { - obj.Height += overflowBottom + contentHeight += overflowBottom } overflowLeft := padding.Left - float64(horizontalPadding) if overflowLeft > 0 { - obj.Width += overflowLeft + contentWidth += overflowLeft dx += overflowLeft } overflowRight := padding.Right - float64(horizontalPadding) if overflowRight > 0 { - obj.Width += overflowRight + contentWidth += overflowRight } - // we need to center children if we have to expand to fit the container label + // manually handle desiredWidth/Height so we can center the grid + var desiredWidth, desiredHeight int + var originalWidthAttr, originalHeightAttr *d2graph.Scalar + if obj.WidthAttr != nil { + desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value) + // SizeToContent without desired width + originalWidthAttr = obj.WidthAttr + obj.WidthAttr = nil + } + if obj.HeightAttr != nil { + desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value) + originalHeightAttr = obj.HeightAttr + obj.HeightAttr = nil + } + // size shape according to grid + obj.SizeToContent(contentWidth, contentHeight, float64(2*horizontalPadding), float64(2*verticalPadding)) + if originalWidthAttr != nil { + obj.WidthAttr = originalWidthAttr + } + if originalHeightAttr != nil { + obj.HeightAttr = originalHeightAttr + } + + if desiredWidth > 0 { + ddx := float64(desiredWidth) - obj.Width + if ddx > 0 { + dx += ddx / 2 + obj.Width = float64(desiredWidth) + } + } + if desiredHeight > 0 { + ddy := float64(desiredHeight) - obj.Height + if ddy > 0 { + dy += ddy / 2 + obj.Height = float64(desiredHeight) + } + } + + // compute where the grid should be placed inside shape + innerBox := obj.ToShape().GetInnerBox() + dx = innerBox.TopLeft.X + dx + dy = innerBox.TopLeft.Y + dy if dx != 0 || dy != 0 { gd.shift(dx, dy) } diff --git a/d2plugin/plugin_features.go b/d2plugin/plugin_features.go index 5ca6aef51..b4331234b 100644 --- a/d2plugin/plugin_features.go +++ b/d2plugin/plugin_features.go @@ -38,7 +38,8 @@ func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error { return fmt.Errorf(`Object "%s" has attribute "top" and/or "left" set, but layout engine "%s" does not support locked positions. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.`, obj.AbsID(), info.Name) } } - if (obj.WidthAttr != nil || obj.HeightAttr != nil) && len(obj.ChildrenArray) > 0 { + if (obj.WidthAttr != nil || obj.HeightAttr != nil) && + len(obj.ChildrenArray) > 0 && !obj.IsGridDiagram() { if _, ok := featureMap[CONTAINER_DIMENSIONS]; !ok { return fmt.Errorf(`Object "%s" has attribute "width" and/or "height" set, but layout engine "%s" does not support dimensions set on containers. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.`, obj.AbsID(), info.Name) } diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index 88d4e3b9d..59a58208d 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -2869,6 +2869,7 @@ y: profits { loadFromFile(t, "grid_edge_across_cell"), loadFromFile(t, "nesting_power"), loadFromFile(t, "unfilled_triangle"), + loadFromFile(t, "grid_container_dimensions"), loadFromFile(t, "grid_label_positions"), } diff --git a/e2etests/testdata/files/grid_container_dimensions.d2 b/e2etests/testdata/files/grid_container_dimensions.d2 new file mode 100644 index 000000000..a09be08b3 --- /dev/null +++ b/e2etests/testdata/files/grid_container_dimensions.d2 @@ -0,0 +1,11 @@ +grid: { + width: 200 + height: 200 + grid-gap: 0 + grid-rows: 2 + grid-columns: 2 + a + b + c + d +} diff --git a/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json b/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json new file mode 100644 index 000000000..a55ca4731 --- /dev/null +++ b/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json @@ -0,0 +1,253 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "grid", + "type": "rectangle", + "pos": { + "x": 0, + "y": 0 + }, + "width": 200, + "height": 200, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "grid", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 44, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "grid.a", + "type": "rectangle", + "pos": { + "x": 46, + "y": 57 + }, + "width": 53, + "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": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.b", + "type": "rectangle", + "pos": { + "x": 99, + "y": 57 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.c", + "type": "rectangle", + "pos": { + "x": 46, + "y": 123 + }, + "width": 53, + "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": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.d", + "type": "rectangle", + "pos": { + "x": 99, + "y": 123 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "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/stable/grid_container_dimensions/dagre/sketch.exp.svg b/e2etests/testdata/stable/grid_container_dimensions/dagre/sketch.exp.svg new file mode 100644 index 000000000..f39bcbbce --- /dev/null +++ b/e2etests/testdata/stable/grid_container_dimensions/dagre/sketch.exp.svg @@ -0,0 +1,106 @@ +gridabcd + + + + + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json b/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json new file mode 100644 index 000000000..f9a8e2442 --- /dev/null +++ b/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json @@ -0,0 +1,253 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "grid", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 200, + "height": 200, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B4", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "grid", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 44, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "grid.a", + "type": "rectangle", + "pos": { + "x": 58, + "y": 69 + }, + "width": 53, + "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": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.b", + "type": "rectangle", + "pos": { + "x": 111, + "y": 69 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.c", + "type": "rectangle", + "pos": { + "x": 58, + "y": 135 + }, + "width": 53, + "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": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "grid.d", + "type": "rectangle", + "pos": { + "x": 111, + "y": 135 + }, + "width": 54, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B5", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 9, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "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/stable/grid_container_dimensions/elk/sketch.exp.svg b/e2etests/testdata/stable/grid_container_dimensions/elk/sketch.exp.svg new file mode 100644 index 000000000..4f85d9a04 --- /dev/null +++ b/e2etests/testdata/stable/grid_container_dimensions/elk/sketch.exp.svg @@ -0,0 +1,106 @@ +gridabcd + + + + + + + \ No newline at end of file