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 @@ +container title is hiddenfirstsecond + + + \ 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