diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 2e4d75c3b..bfb967695 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -802,7 +802,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key)
if reserved == "" {
c.errorf(mk.Range.Start, mk.Range.End, "image shapes cannot have children.")
}
- case d2target.ShapeCircle, d2target.ShapeSquare:
+ case d2target.ShapeCircle, d2target.ShapeSquare, d2target.ShapeDoubleCircle:
checkEqual := (reserved == "width" && obj.Attributes.Height != nil) ||
(reserved == "height" && obj.Attributes.Width != nil)
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 7bd59ec6a..caea9d3a3 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -353,7 +353,7 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string {
shape := obj.Attributes.Shape.Value
- if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) {
+ if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeDoubleCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) {
if level == 1 {
if !obj.IsContainer() {
return theme.Colors.B6
@@ -1101,7 +1101,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
paddingX, paddingY := obj.GetPadding()
switch shapeType {
- case d2target.ShapeSquare, d2target.ShapeCircle:
+ case d2target.ShapeSquare, d2target.ShapeCircle, d2target.ShapeDoubleCircle:
if desiredWidth != 0 || desiredHeight != 0 {
paddingX = 0.
paddingY = 0.
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index cc735055a..0e54a76df 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -493,6 +493,17 @@ func renderOval(tl *geo.Point, width, height float64, style string) string {
return fmt.Sprintf(``, cx, cy, rx, ry, style)
}
+func renderDoubleCircle(tl *geo.Point, width, height float64, style string) string {
+ rx := width / 2
+ ry := height / 2
+ cx := tl.X + rx
+ cy := tl.Y + ry
+ return fmt.Sprintf(`
+ `,
+ cx, cy, rx-2, ry-2, style,
+ cx, cy, rx-10, ry-10, style)
+}
+
func defineShadowFilter(writer io.Writer) {
fmt.Fprint(writer, `
@@ -669,6 +680,19 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
fmt.Fprint(writer, renderOval(tl, width, height, style))
}
+ case d2target.ShapeDoubleCircle:
+ if targetShape.Multiple {
+ fmt.Fprint(writer, renderDoubleCircle(multipleTL, width, height, style))
+ }
+ if sketchRunner != nil {
+ out, err := d2sketch.Oval(sketchRunner, targetShape)
+ if err != nil {
+ return "", err
+ }
+ fmt.Fprintf(writer, out)
+ } else {
+ fmt.Fprint(writer, renderDoubleCircle(tl, width, height, style))
+ }
case d2target.ShapeImage:
fmt.Fprintf(writer, ``,
diff --git a/d2target/d2target.go b/d2target/d2target.go
index 90974c7f1..5c69294ce 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -331,6 +331,7 @@ const (
ShapeSQLTable = "sql_table"
ShapeImage = "image"
ShapeSequenceDiagram = "sequence_diagram"
+ ShapeDoubleCircle = "double_circle"
)
var Shapes = []string{
@@ -357,6 +358,7 @@ var Shapes = []string{
ShapeSQLTable,
ShapeImage,
ShapeSequenceDiagram,
+ ShapeDoubleCircle,
}
func IsShape(s string) bool {
diff --git a/lib/shape/shape.go b/lib/shape/shape.go
index 6422c588f..33db84af7 100644
--- a/lib/shape/shape.go
+++ b/lib/shape/shape.go
@@ -24,6 +24,7 @@ const (
CIRCLE_TYPE = "Circle"
HEXAGON_TYPE = "Hexagon"
CLOUD_TYPE = "Cloud"
+ DOUBLE_CIRCLE_TYPE = "DoubleCircle"
TABLE_TYPE = "Table"
CLASS_TYPE = "Class"
@@ -108,6 +109,8 @@ func NewShape(shapeType string, box *geo.Box) Shape {
return NewCallout(box)
case CIRCLE_TYPE:
return NewCircle(box)
+ case DOUBLE_CIRCLE_TYPE:
+ return NewDoubleCircle(box)
case CLASS_TYPE:
return NewClass(box)
case CLOUD_TYPE:
@@ -164,10 +167,11 @@ func NewShape(shapeType string, box *geo.Box) Shape {
// p is the prev point (used to calculate slope)
// s is the point on the actual shape border that'll be returned
//
-// p
-// │
-// │
-// ▼
+// p
+// │
+// │
+// ▼
+//
// ┌────r─────────────────────────┐
// │ │
// │ │ │
diff --git a/lib/shape/shape_double_circle.go b/lib/shape/shape_double_circle.go
new file mode 100644
index 000000000..d99cbbaa8
--- /dev/null
+++ b/lib/shape/shape_double_circle.go
@@ -0,0 +1,37 @@
+package shape
+
+import (
+ "oss.terrastruct.com/d2/lib/geo"
+ "oss.terrastruct.com/d2/lib/svg"
+)
+
+type shapeDoubleCircle struct {
+ *baseShape
+}
+
+func NewDoubleCircle(box *geo.Box) Shape {
+ return shapeDoubleCircle{
+ baseShape: &baseShape{
+ Type: DOUBLE_CIRCLE_TYPE,
+ Box: box,
+ },
+ }
+}
+
+func doubleCirclePath(box *geo.Box) *svg.SvgPathContext {
+ // halfYFactor := 43.6 / 87.3
+ pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
+ pc.StartAt(pc.Absolute(0.25, 0))
+ // pc
+ return pc
+}
+
+func (s shapeDoubleCircle) Perimeter() []geo.Intersectable {
+ return doubleCirclePath(s.Box).Path
+}
+
+func (s shapeDoubleCircle) GetSVGPathData() []string {
+ return []string{
+ doubleCirclePath(s.Box).PathData(),
+ }
+}