diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 064d91bbe..075d05e7d 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,6 +1,7 @@
#### Features ๐
- PDF exports are now supported. [#120](https://github.com/terrastruct/d2/issues/120)
+- 3D Hexagons are now supported. [#869](https://github.com/terrastruct/d2/issues/869)
#### Improvements ๐งน
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 8cc6452a7..ee1f1df3b 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -615,8 +615,8 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
switch f.Name {
case "style":
if obj.Attributes.Style.ThreeDee != nil {
- if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
- c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`)
+ if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeHexagon) {
+ c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
}
}
if obj.Attributes.Style.DoubleBorder != nil {
diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index 8a8bb33c3..c249844fc 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -1735,7 +1735,7 @@ x.y -> a.b: {
text: `SVP1.shape: oval
SVP1.style.3d: true`,
- expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles`,
+ expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares, rectangles, and hexagons`,
}, {
name: "edge_column_index",
text: `src: {
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 87d403ac0..b5d1f7c82 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -731,6 +731,129 @@ func render3dRect(targetShape d2target.Shape) string {
fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
)
}
+ darkerColor, err := color.Darken(targetShape.Fill)
+ if err != nil {
+ darkerColor = targetShape.Fill
+ }
+ sideShape := d2themes.NewThemableElement("polygon")
+ sideShape.Fill = darkerColor
+ sideShape.Points = strings.Join(sidePoints, " ")
+ sideShape.SetMaskUrl(maskID)
+ sideShape.Style = targetShape.CSSStyle()
+ renderedSides := sideShape.Render()
+
+ return borderMask + mainShapeRendered + renderedSides + renderedBorder
+}
+
+func render3dHexagon(targetShape d2target.Shape) string {
+ moveTo := func(p d2target.Point) string {
+ return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
+ }
+ lineTo := func(p d2target.Point) string {
+ return fmt.Sprintf("L%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
+ }
+ scale := func(n int, f float64) int {
+ return int(float64(n) * f)
+ }
+ halfYFactor := 43.6 / 87.3
+
+ // draw border all in one path to prevent overlapping sections
+ var borderSegments []string
+ // start from the top-left
+ borderSegments = append(borderSegments,
+ moveTo(d2target.Point{X: scale(targetShape.Width, 0.25), Y: 0}),
+ )
+ Y_OFFSET := d2target.THREE_DEE_OFFSET / 2
+ // The following iterates through the sidepoints in clockwise order from top-left, then the main points in clockwise order from bottom-right
+ for _, v := range []d2target.Point{
+ {X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
+ {X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
+ {X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
+ {X: 0, Y: scale(targetShape.Height, halfYFactor)},
+ {X: scale(targetShape.Width, 0.25), Y: 0},
+ {X: scale(targetShape.Width, 0.75), Y: 0},
+ {X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
+ {X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
+ } {
+ borderSegments = append(borderSegments, lineTo(v))
+ }
+ for _, v := range []d2target.Point{
+ {X: scale(targetShape.Width, 0.75), Y: 0},
+ {X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
+ {X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
+ } {
+ borderSegments = append(borderSegments, moveTo(v))
+ borderSegments = append(borderSegments, lineTo(
+ d2target.Point{X: v.X + d2target.THREE_DEE_OFFSET, Y: v.Y - Y_OFFSET},
+ ))
+ }
+ border := d2themes.NewThemableElement("path")
+ border.D = strings.Join(borderSegments, " ")
+ border.Fill = color.None
+ _, borderStroke := d2themes.ShapeTheme(targetShape)
+ border.Stroke = borderStroke
+ borderStyle := targetShape.CSSStyle()
+ border.Style = borderStyle
+ renderedBorder := border.Render()
+
+ var mainPoints []string
+ for _, v := range []d2target.Point{
+ {X: scale(targetShape.Width, 0.25), Y: 0},
+ {X: scale(targetShape.Width, 0.75), Y: 0},
+ {X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
+ {X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
+ {X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
+ {X: 0, Y: scale(targetShape.Height, halfYFactor)},
+ } {
+ mainPoints = append(mainPoints,
+ fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
+ )
+ }
+
+ mainPointsPoly := strings.Join(mainPoints, " ")
+ // create mask from border stroke, to cut away from the shape fills
+ maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID))
+ borderMask := strings.Join([]string{
+ fmt.Sprintf(``,
+ maskID, targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
+ ),
+ fmt.Sprintf(``,
+ targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
+ ),
+ fmt.Sprintf(``,
+ strings.Join(borderSegments, ""), borderStyle),
+ }, "\n")
+ // render the main hexagon without stroke and the border mask
+ mainShape := d2themes.NewThemableElement("polygon")
+ mainShape.X = float64(targetShape.Pos.X)
+ mainShape.Y = float64(targetShape.Pos.Y)
+ mainShape.Points = mainPointsPoly
+ mainShape.SetMaskUrl(maskID)
+ mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
+ mainShape.Fill = mainShapeFill
+ mainShape.Stroke = color.None
+ mainShape.Style = targetShape.CSSStyle()
+ mainShapeRendered := mainShape.Render()
+
+ // render the side shapes in the darkened color without stroke and the border mask
+ var sidePoints []string
+ for _, v := range []d2target.Point{
+ {X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
+ {X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
+ {X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
+ {X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
+ {X: scale(targetShape.Width, 0.75), Y: 0},
+ {X: scale(targetShape.Width, 0.25), Y: 0},
+ } {
+ sidePoints = append(sidePoints,
+ fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
+ )
+ }
// TODO make darker color part of the theme? or just keep this bypass
darkerColor, err := color.Darken(targetShape.Fill)
if err != nil {
@@ -957,6 +1080,39 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
}
}
+ case d2target.ShapeHexagon:
+ if targetShape.ThreeDee {
+ fmt.Fprint(writer, render3dHexagon(targetShape))
+ } else {
+ if targetShape.Multiple {
+ multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
+ el := d2themes.NewThemableElement("path")
+ el.Fill = fill
+ el.Stroke = stroke
+ el.Style = style
+ for _, pathData := range multiplePathData {
+ el.D = pathData
+ fmt.Fprint(writer, el.Render())
+ }
+ }
+
+ if sketchRunner != nil {
+ out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
+ if err != nil {
+ return "", err
+ }
+ fmt.Fprint(writer, out)
+ } else {
+ el := d2themes.NewThemableElement("path")
+ el.Fill = fill
+ el.Stroke = stroke
+ el.Style = style
+ for _, pathData := range s.GetSVGPathData() {
+ el.D = pathData
+ fmt.Fprint(writer, el.Render())
+ }
+ }
+ }
case d2target.ShapeText, d2target.ShapeCode:
default:
if targetShape.Multiple {
diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go
index 615dff3ec..ff7c09820 100644
--- a/e2etests/stable_test.go
+++ b/e2etests/stable_test.go
@@ -155,6 +155,37 @@ rectangle -> square
rectangle.style.3d: true
square.style.3d: true
+`,
+ },
+ {
+ name: "hexagon_3d",
+ script: `
+hexagon: {shape: "hexagon"}
+hexagon.style.3d: true
+`,
+ },
+ {
+ name: "3d_fill_and_stroke",
+ script: `
+hexagon: {
+ shape: hexagon
+ style.3d: true
+ style.fill: honeydew
+}
+
+
+rect: {
+ shape: rectangle
+ style.3d: true
+ style.fill: honeydew
+}
+
+square: {
+ shape: square
+ style.3d: true
+ style.fill: honeydew
+}
+hexagon -> square -> rect
`,
},
{
diff --git a/e2etests/testdata/stable/3d_fill_and_stroke/dagre/board.exp.json b/e2etests/testdata/stable/3d_fill_and_stroke/dagre/board.exp.json
new file mode 100644
index 000000000..95e267b57
--- /dev/null
+++ b/e2etests/testdata/stable/3d_fill_and_stroke/dagre/board.exp.json
@@ -0,0 +1,227 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "rect",
+ "type": "rectangle",
+ "pos": {
+ "x": 28,
+ "y": 363
+ },
+ "width": 73,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "rect",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 28,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "square",
+ "type": "rectangle",
+ "pos": {
+ "x": 17,
+ "y": 169
+ },
+ "width": 94,
+ "height": 94,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "square",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 49,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(hexagon -> square)[0]",
+ "src": "hexagon",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "square",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 64,
+ "y": 69
+ },
+ {
+ "x": 64,
+ "y": 109
+ },
+ {
+ "x": 64,
+ "y": 129
+ },
+ {
+ "x": 64,
+ "y": 169
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(square -> rect)[0]",
+ "src": "square",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "rect",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 64,
+ "y": 263
+ },
+ {
+ "x": 64,
+ "y": 303
+ },
+ {
+ "x": 64,
+ "y": 323
+ },
+ {
+ "x": 64,
+ "y": 363
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/3d_fill_and_stroke/dagre/sketch.exp.svg b/e2etests/testdata/stable/3d_fill_and_stroke/dagre/sketch.exp.svg
new file mode 100644
index 000000000..02573519f
--- /dev/null
+++ b/e2etests/testdata/stable/3d_fill_and_stroke/dagre/sketch.exp.svg
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/3d_fill_and_stroke/elk/board.exp.json b/e2etests/testdata/stable/3d_fill_and_stroke/elk/board.exp.json
new file mode 100644
index 000000000..8ae77b5da
--- /dev/null
+++ b/e2etests/testdata/stable/3d_fill_and_stroke/elk/board.exp.json
@@ -0,0 +1,209 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "rect",
+ "type": "rectangle",
+ "pos": {
+ "x": 39,
+ "y": 315
+ },
+ "width": 73,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "rect",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 28,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "square",
+ "type": "rectangle",
+ "pos": {
+ "x": 29,
+ "y": 151
+ },
+ "width": 94,
+ "height": 94,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "honeydew",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "square",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 49,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(hexagon -> square)[0]",
+ "src": "hexagon",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "square",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 76,
+ "y": 81
+ },
+ {
+ "x": 76,
+ "y": 151
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(square -> rect)[0]",
+ "src": "square",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "rect",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 76,
+ "y": 245
+ },
+ {
+ "x": 76,
+ "y": 315
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/3d_fill_and_stroke/elk/sketch.exp.svg b/e2etests/testdata/stable/3d_fill_and_stroke/elk/sketch.exp.svg
new file mode 100644
index 000000000..3ea48ed52
--- /dev/null
+++ b/e2etests/testdata/stable/3d_fill_and_stroke/elk/sketch.exp.svg
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/3d_sketch_mode/dagre/board.exp.json b/e2etests/testdata/stable/3d_sketch_mode/dagre/board.exp.json
new file mode 100644
index 000000000..094c34caf
--- /dev/null
+++ b/e2etests/testdata/stable/3d_sketch_mode/dagre/board.exp.json
@@ -0,0 +1,138 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "rect",
+ "type": "rectangle",
+ "pos": {
+ "x": 28,
+ "y": 169
+ },
+ "width": 73,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "rect",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 28,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(hexagon -> rect)[0]",
+ "src": "hexagon",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "rect",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 64,
+ "y": 69
+ },
+ {
+ "x": 64,
+ "y": 109
+ },
+ {
+ "x": 64,
+ "y": 129
+ },
+ {
+ "x": 64,
+ "y": 169
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/3d_sketch_mode/dagre/sketch.exp.svg b/e2etests/testdata/stable/3d_sketch_mode/dagre/sketch.exp.svg
new file mode 100644
index 000000000..4fbaf9562
--- /dev/null
+++ b/e2etests/testdata/stable/3d_sketch_mode/dagre/sketch.exp.svg
@@ -0,0 +1,47 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/3d_sketch_mode/elk/board.exp.json b/e2etests/testdata/stable/3d_sketch_mode/elk/board.exp.json
new file mode 100644
index 000000000..ee2228676
--- /dev/null
+++ b/e2etests/testdata/stable/3d_sketch_mode/elk/board.exp.json
@@ -0,0 +1,129 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "rect",
+ "type": "rectangle",
+ "pos": {
+ "x": 39,
+ "y": 151
+ },
+ "width": 73,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "rect",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 28,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(hexagon -> rect)[0]",
+ "src": "hexagon",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "rect",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 76,
+ "y": 81
+ },
+ {
+ "x": 76,
+ "y": 151
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/3d_sketch_mode/elk/sketch.exp.svg b/e2etests/testdata/stable/3d_sketch_mode/elk/sketch.exp.svg
new file mode 100644
index 000000000..b9cf1fad3
--- /dev/null
+++ b/e2etests/testdata/stable/3d_sketch_mode/elk/sketch.exp.svg
@@ -0,0 +1,47 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/hexagon_3d/dagre/board.exp.json b/e2etests/testdata/stable/hexagon_3d/dagre/board.exp.json
new file mode 100644
index 000000000..7abe6c9ff
--- /dev/null
+++ b/e2etests/testdata/stable/hexagon_3d/dagre/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/stable/hexagon_3d/dagre/sketch.exp.svg b/e2etests/testdata/stable/hexagon_3d/dagre/sketch.exp.svg
new file mode 100644
index 000000000..37f515d9d
--- /dev/null
+++ b/e2etests/testdata/stable/hexagon_3d/dagre/sketch.exp.svg
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/hexagon_3d/elk/board.exp.json b/e2etests/testdata/stable/hexagon_3d/elk/board.exp.json
new file mode 100644
index 000000000..69e00a8d1
--- /dev/null
+++ b/e2etests/testdata/stable/hexagon_3d/elk/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "hexagon",
+ "type": "hexagon",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 128,
+ "height": 69,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "N5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "hexagon",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/stable/hexagon_3d/elk/sketch.exp.svg b/e2etests/testdata/stable/hexagon_3d/elk/sketch.exp.svg
new file mode 100644
index 000000000..5f1cf80e7
--- /dev/null
+++ b/e2etests/testdata/stable/hexagon_3d/elk/sketch.exp.svg
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/testdata/d2compiler/TestCompile/3d_oval.exp.json b/testdata/d2compiler/TestCompile/3d_oval.exp.json
index e237f09c0..997f4d61c 100644
--- a/testdata/d2compiler/TestCompile/3d_oval.exp.json
+++ b/testdata/d2compiler/TestCompile/3d_oval.exp.json
@@ -5,7 +5,7 @@
"errs": [
{
"range": "d2/testdata/d2compiler/TestCompile/3d_oval.d2,1:0:17-1:19:36",
- "errmsg": "d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key \"3d\" can only be applied to squares and rectangles"
+ "errmsg": "d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key \"3d\" can only be applied to squares, rectangles, and hexagons"
}
]
}