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(), + } +}