diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 2ca488754..35c26ac0e 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -18,6 +18,7 @@ There's also been a major compiler rewrite. It's fixed many minor compiler bugs, - Reduces default padding of shapes. [#702](https://github.com/terrastruct/d2/pull/702) - Ensures labels fit inside shapes with shape-specific inner bounding boxes. [#702](https://github.com/terrastruct/d2/pull/702) - dagre container labels changed positions to outside the shape. Many previously obscured container labels are now legible. [#788](https://github.com/terrastruct/d2/pull/788) +- Container icons are placed top-left instead of center, to ensure no collisions with children. [#806](https://github.com/terrastruct/d2/pull/806) - Code snippets use bold and italic font styles as determined by highlighter [#710](https://github.com/terrastruct/d2/issues/710), [#741](https://github.com/terrastruct/d2/issues/741) - Improves package shape dimensions with short height. [#702](https://github.com/terrastruct/d2/pull/702) - Sequence diagrams are rendered more compacted, both vertically and horizontally. [#796](https://github.com/terrastruct/d2/pull/796) diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go index a3d943200..d1c91f2ed 100644 --- a/d2layouts/d2dagrelayout/layout.go +++ b/d2layouts/d2dagrelayout/layout.go @@ -109,12 +109,21 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err maxContainerLabelHeight := 0 for _, obj := range g.Objects { - if len(obj.ChildrenArray) == 0 { + if len(obj.ChildrenArray) == 0 || obj.Parent == g.Root { continue } if obj.LabelHeight != nil { maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING) } + + if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage { + contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height)) + shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value] + s := shape.NewShape(shapeType, contentBox) + iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) + // Since dagre container labels are pushed up, we don't want a child container to collide + maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, (iconSize+label.PADDING*2)*2) + } } maxLabelSize := 0 @@ -219,7 +228,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err } } if obj.Attributes.Icon != nil { - obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter)) + if len(obj.ChildrenArray) > 0 { + obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft)) + obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight)) + } else { + obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter)) + } } } diff --git a/d2layouts/d2elklayout/layout.go b/d2layouts/d2elklayout/layout.go index 886d3424a..7e1703b05 100644 --- a/d2layouts/d2elklayout/layout.go +++ b/d2layouts/d2elklayout/layout.go @@ -199,6 +199,24 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err Padding: opts.Padding, }, } + + if n.LayoutOptions.Padding == DefaultOpts.Padding { + // Default + paddingTop := 50 + if obj.LabelHeight != nil { + paddingTop = go2.Max(paddingTop, *obj.LabelHeight+label.PADDING) + } + if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage { + contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height)) + shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value] + s := shape.NewShape(shapeType, contentBox) + iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) + paddingTop = go2.Max(paddingTop, iconSize+label.PADDING*2) + } + n.LayoutOptions.Padding = fmt.Sprintf("[top=%d,left=50,bottom=50,right=50]", + paddingTop, + ) + } } if obj.LabelWidth != nil && obj.LabelHeight != nil { @@ -310,7 +328,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err } } if obj.Attributes.Icon != nil { - obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter)) + if len(obj.ChildrenArray) > 0 { + obj.IconPosition = go2.Pointer(string(label.InsideTopLeft)) + obj.LabelPosition = go2.Pointer(string(label.InsideTopRight)) + } else { + obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter)) + } } byID[obj.AbsID()] = obj diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 25c5dd88e..0b5192b7c 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -883,7 +883,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske } else { box = s.GetInnerBox() } - iconSize := targetShape.GetIconSize(box) + iconSize := d2target.GetIconSize(box, targetShape.IconPosition) tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize)) diff --git a/d2target/d2target.go b/d2target/d2target.go index e5cf20cdb..20207c121 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -538,8 +538,8 @@ func init() { } } -func (s *Shape) GetIconSize(box *geo.Box) int { - iconPosition := label.Position(s.IconPosition) +func GetIconSize(box *geo.Box, position string) int { + iconPosition := label.Position(position) minDimension := int(math.Min(box.Width, box.Height)) halfMinDimension := int(math.Ceil(0.5 * float64(minDimension))) diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index bca51d2e7..7f9666e9e 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -895,6 +895,37 @@ b: { icon: https://icons.terrastruct.com/essentials/004-picture.svg } a -> b +`, + }, + { + name: "icon-containers", + script: `vpc: VPC 1 10.1.0.0./16 { + icon: https://icons.terrastruct.com/aws%2F_Group%20Icons%2FVirtual-private-cloud-VPC_light-bg.svg + style: { + stroke: green + font-color: green + fill: white + } + az: Availability Zone A { + style: { + stroke: blue + font-color: blue + stroke-dash: 3 + fill: white + } + firewall: Firewall Subnet A { + icon: https://icons.terrastruct.com/aws%2FNetworking%20&%20Content%20Delivery%2FAmazon-Route-53_Hosted-Zone_light-bg.svg + style: { + stroke: purple + font-color: purple + fill: "#e1d5e7" + } + ec2: EC2 Instance { + icon: https://icons.terrastruct.com/aws%2FCompute%2F_Instance%2FAmazon-EC2_C4-Instance_light-bg.svg + } + } + } +} `, }, { diff --git a/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json b/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json index 74443f63c..22a8aacd9 100644 --- a/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json +++ b/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json @@ -7,10 +7,10 @@ "type": "rectangle", "pos": { "x": 0, - "y": 65 + "y": 50 }, "width": 264, - "height": 511, + "height": 406, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -48,10 +48,10 @@ "type": "rectangle", "pos": { "x": 20, - "y": 162 + "y": 125 }, "width": 224, - "height": 381, + "height": 306, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -89,10 +89,10 @@ "type": "rectangle", "pos": { "x": 40, - "y": 241 + "y": 196 }, "width": 184, - "height": 270, + "height": 210, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -130,10 +130,10 @@ "type": "rectangle", "pos": { "x": 60, - "y": 309 + "y": 249 }, "width": 144, - "height": 160, + "height": 130, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -171,7 +171,7 @@ "type": "rectangle", "pos": { "x": 100, - "y": 361 + "y": 286 }, "width": 64, "height": 56, diff --git a/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg b/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg index 61db3ed3b..167c42957 100644 --- a/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="652" height="843" viewBox="-194 -165 652 843">VPC 1 10.1.0.0./16Availability Zone AFirewall Subnet AEC2 Instance + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/icon-containers/elk/board.exp.json b/e2etests/testdata/stable/icon-containers/elk/board.exp.json new file mode 100644 index 000000000..2b4d6a338 --- /dev/null +++ b/e2etests/testdata/stable/icon-containers/elk/board.exp.json @@ -0,0 +1,204 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "vpc", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 460, + "height": 466, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "white", + "stroke": "green", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": { + "Scheme": "https", + "Opaque": "", + "User": null, + "Host": "icons.terrastruct.com", + "Path": "/aws/_Group Icons/Virtual-private-cloud-VPC_light-bg.svg", + "RawPath": "/aws%2F_Group%20Icons%2FVirtual-private-cloud-VPC_light-bg.svg", + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "iconPosition": "INSIDE_TOP_LEFT", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "VPC 1 10.1.0.0./16", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "green", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 206, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_RIGHT", + "zIndex": 0, + "level": 1 + }, + { + "id": "vpc.az", + "type": "rectangle", + "pos": { + "x": 62, + "y": 86 + }, + "width": 360, + "height": 342, + "opacity": 1, + "strokeDash": 3, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "white", + "stroke": "blue", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Availability Zone A", + "fontSize": 24, + "fontFamily": "DEFAULT", + "language": "", + "color": "blue", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 185, + "labelHeight": 31, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "vpc.az.firewall", + "type": "rectangle", + "pos": { + "x": 112, + "y": 136 + }, + "width": 260, + "height": 242, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#e1d5e7", + "stroke": "purple", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": { + "Scheme": "https", + "Opaque": "", + "User": null, + "Host": "icons.terrastruct.com", + "Path": "/aws/Networking & Content Delivery/Amazon-Route-53_Hosted-Zone_light-bg.svg", + "RawPath": "/aws%2FNetworking%20&%20Content%20Delivery%2FAmazon-Route-53_Hosted-Zone_light-bg.svg", + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "iconPosition": "INSIDE_TOP_LEFT", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "Firewall Subnet A", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "purple", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 145, + "labelHeight": 26, + "labelPosition": "INSIDE_TOP_RIGHT", + "zIndex": 0, + "level": 3 + }, + { + "id": "vpc.az.firewall.ec2", + "type": "rectangle", + "pos": { + "x": 162, + "y": 210 + }, + "width": 160, + "height": 118, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": { + "Scheme": "https", + "Opaque": "", + "User": null, + "Host": "icons.terrastruct.com", + "Path": "/aws/Compute/_Instance/Amazon-EC2_C4-Instance_light-bg.svg", + "RawPath": "/aws%2FCompute%2F_Instance%2FAmazon-EC2_C4-Instance_light-bg.svg", + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "iconPosition": "INSIDE_MIDDLE_CENTER", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "EC2 Instance", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 89, + "labelHeight": 21, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 4 + } + ], + "connections": [] +} diff --git a/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg b/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg new file mode 100644 index 000000000..8fb5bdd7f --- /dev/null +++ b/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg @@ -0,0 +1,59 @@ + +VPC 1 10.1.0.0./16Availability Zone AFirewall Subnet AEC2 Instance + + + \ No newline at end of file diff --git a/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json b/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json index 46d342706..9e7c7e58c 100644 --- a/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json +++ b/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json @@ -35,7 +35,7 @@ "Fragment": "", "RawFragment": "" }, - "iconPosition": "INSIDE_MIDDLE_CENTER", + "iconPosition": "OUTSIDE_TOP_LEFT", "blend": false, "fields": null, "methods": null, @@ -50,7 +50,7 @@ "underline": false, "labelWidth": 96, "labelHeight": 38, - "labelPosition": "OUTSIDE_TOP_CENTER", + "labelPosition": "OUTSIDE_TOP_RIGHT", "zIndex": 0, "level": 1 }, diff --git a/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg b/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg index a515d2ca5..469647c61 100644 --- a/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg +++ b/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="377" height="800" viewBox="-102 -100 377 800">