diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 06411bc21..c90ee7607 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -18,6 +18,7 @@ import (
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/geo"
+ "oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@@ -1117,14 +1118,14 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
- shapeType := strings.ToLower(obj.Attributes.Shape.Value)
+ dslShape := strings.ToLower(obj.Attributes.Shape.Value)
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
return err
}
- switch shapeType {
+ switch dslShape {
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
// no labels
default:
@@ -1134,7 +1135,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
- if shapeType != d2target.ShapeText && obj.Attributes.Label.Value != "" {
+ if dslShape != d2target.ShapeText && obj.Attributes.Label.Value != "" {
labelDims.Width += INNER_LABEL_PADDING
labelDims.Height += INNER_LABEL_PADDING
}
@@ -1150,7 +1151,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
paddingX, paddingY := obj.GetPadding()
- switch shapeType {
+ switch dslShape {
case d2target.ShapeSquare, d2target.ShapeCircle:
if desiredWidth != 0 || desiredHeight != 0 {
paddingX = 0.
@@ -1169,6 +1170,12 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
obj.Height += float64(paddingY)
}
}
+ 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)
+ newWidth, newHeight := s.GetDimensionsToFit(contentBox.Width, contentBox.Height, paddingX/2)
+ obj.Width = newWidth
+ obj.Height = newHeight
}
for _, edge := range g.Edges {
endpointLabels := []string{}
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index e5a3412e4..eac72d4a4 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -42,7 +42,7 @@ const (
appendixIconRadius = 16
)
-var multipleOffset = geo.NewVector(10, -10)
+var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET)
//go:embed tooltip.svg
var TooltipIcon string
@@ -734,7 +734,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
var multipleTL *geo.Point
if targetShape.Multiple {
- multipleTL = tl.AddVector(geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET))
+ multipleTL = tl.AddVector(multipleOffset)
}
switch targetShape.Type {
@@ -744,13 +744,13 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
drawClass(writer, targetShape)
}
addAppendixItems(writer, targetShape)
- fmt.Fprintf(writer, ``)
- fmt.Fprintf(writer, closingTag)
+ fmt.Fprint(writer, ``)
+ fmt.Fprint(writer, closingTag)
return labelMask, nil
case d2target.ShapeSQLTable:
if sketchRunner != nil {
@@ -758,13 +758,13 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
drawTable(writer, targetShape)
}
addAppendixItems(writer, targetShape)
- fmt.Fprintf(writer, ``)
- fmt.Fprintf(writer, closingTag)
+ fmt.Fprint(writer, ``)
+ fmt.Fprint(writer, closingTag)
return labelMask, nil
case d2target.ShapeOval:
if targetShape.DoubleBorder {
@@ -776,7 +776,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderDoubleOval(tl, width, height, style))
}
@@ -789,12 +789,17 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderOval(tl, width, height, style))
}
}
+ // debugging
+ for _, pathData := range s.GetSVGPathData() {
+ fmt.Fprintf(writer, ``, pathData, style)
+ }
+
case d2target.ShapeImage:
fmt.Fprintf(writer, ``,
html.EscapeString(targetShape.Icon.String()),
@@ -815,7 +820,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
fmt.Fprintf(writer, ``,
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
@@ -832,7 +837,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
fmt.Fprintf(writer, ``,
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
@@ -855,7 +860,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil {
return "", err
}
- fmt.Fprintf(writer, out)
+ fmt.Fprint(writer, out)
} else {
for _, pathData := range s.GetSVGPathData() {
fmt.Fprintf(writer, ``, pathData, style)
@@ -864,7 +869,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
// Closes the class=shape
- fmt.Fprintf(writer, ``)
+ fmt.Fprint(writer, ``)
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
iconPosition := label.Position(targetShape.IconPosition)
@@ -895,7 +900,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
box = s.GetInnerBox()
}
- labelTL := labelPosition.GetPointOnBox(box, label.PADDING, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight))
+ labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
+ float64(targetShape.LabelWidth),
+ // TODO consider further
+ float64(targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING),
+ )
fontClass := "text"
if targetShape.Bold {
@@ -932,7 +941,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
fmt.Fprintf(writer, ``,
targetShape.Width, targetShape.Height, containerStyle)
// Padding
- fmt.Fprintf(writer, ``)
+ fmt.Fprint(writer, ``)
for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
// TODO mono font looks better with 1.2 em (use px equivalent), but textmeasure needs to account for it. Not obvious how that should be done
@@ -947,7 +956,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, "")
}
- fmt.Fprintf(writer, "")
+ fmt.Fprint(writer, "")
} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
render, err := d2latex.Render(targetShape.Label)
if err != nil {
@@ -955,7 +964,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprintf(writer, ``, box.TopLeft.X, box.TopLeft.Y)
fmt.Fprint(writer, render)
- fmt.Fprintf(writer, "")
+ fmt.Fprint(writer, "")
} else if targetShape.Type == d2target.ShapeText && targetShape.Language != "" {
render, err := textmeasure.RenderMarkdown(targetShape.Label)
if err != nil {
@@ -1000,7 +1009,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
addAppendixItems(writer, targetShape)
- fmt.Fprintf(writer, closingTag)
+ fmt.Fprint(writer, closingTag)
return labelMask, nil
}
@@ -1230,7 +1239,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
fmt.Fprintf(buf, ``, mdCSS)
}
if sketchRunner != nil {
- fmt.Fprintf(buf, d2sketch.DefineFillPattern())
+ fmt.Fprint(buf, d2sketch.DefineFillPattern())
}
// only define shadow filter if a shape uses it
diff --git a/lib/shape/shape.go b/lib/shape/shape.go
index 6422c588f..1585bad7b 100644
--- a/lib/shape/shape.go
+++ b/lib/shape/shape.go
@@ -4,6 +4,7 @@ import (
"math"
"oss.terrastruct.com/d2/lib/geo"
+ "oss.terrastruct.com/d2/lib/svg"
)
const (
@@ -90,6 +91,8 @@ func (s baseShape) GetInnerTopLeft(_, _, padding float64) geo.Point {
return *geo.NewPoint(s.Box.TopLeft.X+padding, s.Box.TopLeft.Y+padding)
}
+// return the minimum shape dimensions needed to fit content (width x height)
+// in the shape's innerBox with padding
func (s baseShape) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
return width + padding*2, height + padding*2
}
@@ -209,3 +212,13 @@ func TraceToShapeBorder(shape Shape, rectBorderPoint, prevPoint *geo.Point) *geo
return geo.NewPoint(math.Round(closestPoint.X), math.Round(closestPoint.Y))
}
+
+func boxPath(box *geo.Box) *svg.SvgPathContext {
+ pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
+ pc.StartAt(pc.Absolute(0, 0))
+ pc.L(false, box.Width, 0)
+ pc.L(false, box.Width, box.Height)
+ pc.L(false, 0, box.Height)
+ pc.Z()
+ return pc
+}
diff --git a/lib/shape/shape_callout.go b/lib/shape/shape_callout.go
index 73cbaac58..0942592df 100644
--- a/lib/shape/shape_callout.go
+++ b/lib/shape/shape_callout.go
@@ -9,6 +9,11 @@ type shapeCallout struct {
*baseShape
}
+const (
+ defaultTipWidth = 30.
+ defaultTipHeight = 45.
+)
+
func NewCallout(box *geo.Box) Shape {
return shapeCallout{
baseShape: &baseShape{
@@ -18,25 +23,31 @@ func NewCallout(box *geo.Box) Shape {
}
}
-func (s shapeCallout) GetInnerBox() *geo.Box {
- height := s.Box.Height
- tipHeight := 45.0
- if height < tipHeight*2 {
- tipHeight = height / 2.0
+func getTipWidth(box *geo.Box) float64 {
+ tipWidth := defaultTipWidth
+ if box.Width < tipWidth*2 {
+ tipWidth = box.Width / 2.0
}
- height -= tipHeight
+ return tipWidth
+}
+
+func getTipHeight(box *geo.Box) float64 {
+ tipHeight := defaultTipHeight
+ if box.Height < tipHeight*2 {
+ tipHeight = box.Height / 2.0
+ }
+ return tipHeight
+}
+
+func (s shapeCallout) GetInnerBox() *geo.Box {
+ tipHeight := getTipHeight(s.Box)
+ height := s.Box.Height - tipHeight
return geo.NewBox(s.Box.TopLeft.Copy(), s.Box.Width, height)
}
func calloutPath(box *geo.Box) *svg.SvgPathContext {
- tipWidth := 30.0
- if box.Width < tipWidth*2 {
- tipWidth = box.Width / 2.0
- }
- tipHeight := 45.0
- if box.Height < tipHeight*2 {
- tipHeight = box.Height / 2.0
- }
+ tipWidth := getTipWidth(box)
+ tipHeight := getTipHeight(box)
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
pc.StartAt(pc.Absolute(0, 0))
pc.V(true, box.Height-tipHeight)
@@ -57,5 +68,19 @@ func (s shapeCallout) Perimeter() []geo.Intersectable {
func (s shapeCallout) GetSVGPathData() []string {
return []string{
calloutPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeCallout) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ // return the minimum shape dimensions needed to fit content (width x height)
+ // in the shape's innerBox with padding
+ baseHeight := height + padding*2
+ if baseHeight < defaultTipHeight {
+ baseHeight *= 2
+ } else {
+ baseHeight += defaultTipHeight
+ }
+ return width + padding*2, baseHeight
+}
diff --git a/lib/shape/shape_circle.go b/lib/shape/shape_circle.go
index 50ab2cc77..54d981099 100644
--- a/lib/shape/shape_circle.go
+++ b/lib/shape/shape_circle.go
@@ -19,13 +19,23 @@ func NewCircle(box *geo.Box) Shape {
}
}
+func (s shapeCircle) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ height := s.Box.Height
+ insideTL := s.GetInsidePlacement(width, height, 0)
+ tl := s.Box.TopLeft.Copy()
+ width -= 2 * (insideTL.X - tl.X)
+ height -= 2 * (insideTL.Y - tl.Y)
+ return geo.NewBox(&insideTL, width, height)
+}
+
func (s shapeCircle) AspectRatio1() bool {
return true
}
func (s shapeCircle) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
- radius := math.Ceil(math.Sqrt(math.Pow(width/2, 2)+math.Pow(height/2, 2))) + padding
- return radius * 2, radius * 2
+ diameter := math.Ceil(math.Sqrt(2 * math.Pow(math.Max(width, height)+2*padding, 2)))
+ return diameter, diameter
}
func (s shapeCircle) GetInsidePlacement(width, height, padding float64) geo.Point {
diff --git a/lib/shape/shape_cloud.go b/lib/shape/shape_cloud.go
index fb200feaf..ffa8e18ae 100644
--- a/lib/shape/shape_cloud.go
+++ b/lib/shape/shape_cloud.go
@@ -40,6 +40,24 @@ func NewCloud(box *geo.Box) Shape {
}
}
+func (s shapeCloud) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ height := s.Box.Height
+ insideTL := s.GetInsidePlacement(width, height, 0)
+ aspectRatio := width / height
+ if aspectRatio > CLOUD_WIDE_ASPECT_BOUNDARY {
+ width *= CLOUD_WIDE_INNER_WIDTH
+ height *= CLOUD_WIDE_INNER_HEIGHT
+ } else if aspectRatio < CLOUD_TALL_ASPECT_BOUNDARY {
+ width *= CLOUD_TALL_INNER_WIDTH
+ height *= CLOUD_TALL_INNER_HEIGHT
+ } else {
+ width *= CLOUD_SQUARE_INNER_WIDTH
+ height *= CLOUD_SQUARE_INNER_HEIGHT
+ }
+ return geo.NewBox(&insideTL, width, height)
+}
+
func (s shapeCloud) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
width += padding
height += padding
@@ -96,5 +114,7 @@ func (s shapeCloud) Perimeter() []geo.Intersectable {
func (s shapeCloud) GetSVGPathData() []string {
return []string{
cloudPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
diff --git a/lib/shape/shape_cylinder.go b/lib/shape/shape_cylinder.go
index dfc843841..10ebaf16c 100644
--- a/lib/shape/shape_cylinder.go
+++ b/lib/shape/shape_cylinder.go
@@ -9,6 +9,10 @@ type shapeCylinder struct {
*baseShape
}
+const (
+ defaultArcDepth = 24.
+)
+
func NewCylinder(box *geo.Box) Shape {
return shapeCylinder{
baseShape: &baseShape{
@@ -18,46 +22,47 @@ func NewCylinder(box *geo.Box) Shape {
}
}
+func getArcHeight(box *geo.Box) float64 {
+ arcHeight := defaultArcDepth
+ // Note: box height should always be larger than 3*default
+ // this just handles after collapsing into an oval
+ if box.Height < arcHeight*2 {
+ arcHeight = box.Height / 2.0
+ }
+ return arcHeight
+}
+
func (s shapeCylinder) GetInnerBox() *geo.Box {
height := s.Box.Height
tl := s.Box.TopLeft.Copy()
- arcDepth := 24.0
- if height < arcDepth*2 {
- arcDepth = height / 2.0
- }
- height -= 3 * arcDepth
- tl.Y += 2 * arcDepth
+ arc := getArcHeight(s.Box)
+ height -= 3 * arc
+ tl.Y += 2 * arc
return geo.NewBox(tl, s.Box.Width, height)
}
func cylinderOuterPath(box *geo.Box) *svg.SvgPathContext {
- arcDepth := 24.0
- if box.Height < arcDepth*2 {
- arcDepth = box.Height / 2
- }
+ arcHeight := getArcHeight(box)
multiplier := 0.45
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
- pc.StartAt(pc.Absolute(0, arcDepth))
+ pc.StartAt(pc.Absolute(0, arcHeight))
pc.C(false, 0, 0, box.Width*multiplier, 0, box.Width/2, 0)
- pc.C(false, box.Width-box.Width*multiplier, 0, box.Width, 0, box.Width, arcDepth)
- pc.V(true, box.Height-arcDepth*2)
+ pc.C(false, box.Width-box.Width*multiplier, 0, box.Width, 0, box.Width, arcHeight)
+ pc.V(true, box.Height-arcHeight*2)
pc.C(false, box.Width, box.Height, box.Width-box.Width*multiplier, box.Height, box.Width/2, box.Height)
- pc.C(false, box.Width*multiplier, box.Height, 0, box.Height, 0, box.Height-arcDepth)
- pc.V(true, -(box.Height - arcDepth*2))
+ pc.C(false, box.Width*multiplier, box.Height, 0, box.Height, 0, box.Height-arcHeight)
+ pc.V(true, -(box.Height - arcHeight*2))
pc.Z()
return pc
}
func cylinderInnerPath(box *geo.Box) *svg.SvgPathContext {
- arcDepth := 24.0
- if box.Height < arcDepth*2 {
- arcDepth = box.Height / 2
- }
+ arcHeight := getArcHeight(box)
multiplier := 0.45
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
- pc.StartAt(pc.Absolute(0, arcDepth))
- pc.C(false, 0, arcDepth*2, box.Width*multiplier, arcDepth*2, box.Width/2, arcDepth*2)
- pc.C(false, box.Width-box.Width*multiplier, arcDepth*2, box.Width, arcDepth*2, box.Width, arcDepth)
+ pc.StartAt(pc.Absolute(0, arcHeight))
+ pc.C(false, 0, arcHeight*2, box.Width*multiplier, arcHeight*2, box.Width/2, arcHeight*2)
+ pc.C(false, box.Width-box.Width*multiplier, arcHeight*2, box.Width, arcHeight*2, box.Width, arcHeight)
return pc
}
@@ -69,5 +74,13 @@ func (s shapeCylinder) GetSVGPathData() []string {
return []string{
cylinderOuterPath(s.Box).PathData(),
cylinderInnerPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeCylinder) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ // 2 arcs top, height + padding, 1 arc bottom
+ totalHeight := height + padding*2 + 3*defaultArcDepth
+ return width + padding*2, totalHeight
+}
diff --git a/lib/shape/shape_diamond.go b/lib/shape/shape_diamond.go
index 9fb115194..02d8a0cd2 100644
--- a/lib/shape/shape_diamond.go
+++ b/lib/shape/shape_diamond.go
@@ -18,6 +18,17 @@ func NewDiamond(box *geo.Box) Shape {
}
}
+func (s shapeDiamond) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ height := s.Box.Height
+ tl := s.Box.TopLeft.Copy()
+ tl.X += width / 4.
+ tl.Y += height / 4.
+ width /= 2.
+ height /= 2.
+ return geo.NewBox(tl, width, height)
+}
+
func diamondPath(box *geo.Box) *svg.SvgPathContext {
pc := svg.NewSVGPathContext(box.TopLeft, box.Width/77, box.Height/76.9)
pc.StartAt(pc.Absolute(38.5, 76.9))
@@ -41,5 +52,13 @@ func (s shapeDiamond) Perimeter() []geo.Intersectable {
func (s shapeDiamond) GetSVGPathData() []string {
return []string{
diamondPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeDiamond) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := 2 * (width + 2*padding)
+ totalHeight := 2 * (height + 2*padding)
+ return totalWidth, totalHeight
+}
diff --git a/lib/shape/shape_document.go b/lib/shape/shape_document.go
index 3cdb5d100..efda39f86 100644
--- a/lib/shape/shape_document.go
+++ b/lib/shape/shape_document.go
@@ -9,6 +9,13 @@ type shapeDocument struct {
*baseShape
}
+const (
+ // the shape is taller than where the bottom of the path ends
+ docPathHeight = 18.925
+ docPathInnerBottom = 14
+ docPathBottom = 16.3
+)
+
func NewDocument(box *geo.Box) Shape {
return shapeDocument{
baseShape: &baseShape{
@@ -18,15 +25,19 @@ func NewDocument(box *geo.Box) Shape {
}
}
+func (s shapeDocument) GetInnerBox() *geo.Box {
+ height := s.Box.Height * docPathInnerBottom / docPathHeight
+ return geo.NewBox(s.Box.TopLeft.Copy(), s.Box.Width, height)
+}
+
func documentPath(box *geo.Box) *svg.SvgPathContext {
- pathHeight := 18.925
pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
- pc.StartAt(pc.Absolute(0, 16.3/pathHeight))
+ pc.StartAt(pc.Absolute(0, docPathBottom/docPathHeight))
pc.L(false, 0, 0)
pc.L(false, 1, 0)
- pc.L(false, 1, 16.3/pathHeight)
- pc.C(false, 5/6.0, 12.8/pathHeight, 2/3.0, 12.8/pathHeight, 1/2.0, 16.3/pathHeight)
- pc.C(false, 1/3.0, 19.8/pathHeight, 1/6.0, 19.8/pathHeight, 0, 16.3/pathHeight)
+ pc.L(false, 1, docPathBottom/docPathHeight)
+ pc.C(false, 5/6.0, 12.8/docPathHeight, 2/3.0, 12.8/docPathHeight, 1/2.0, docPathBottom/docPathHeight)
+ pc.C(false, 1/3.0, 19.8/docPathHeight, 1/6.0, 19.8/docPathHeight, 0, docPathBottom/docPathHeight)
pc.Z()
return pc
}
@@ -38,5 +49,12 @@ func (s shapeDocument) Perimeter() []geo.Intersectable {
func (s shapeDocument) GetSVGPathData() []string {
return []string{
documentPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeDocument) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ baseHeight := (height + padding*2) * docPathHeight / docPathInnerBottom
+ return width + padding*2, baseHeight
+}
diff --git a/lib/shape/shape_hexagon.go b/lib/shape/shape_hexagon.go
index b726181a2..203904be1 100644
--- a/lib/shape/shape_hexagon.go
+++ b/lib/shape/shape_hexagon.go
@@ -18,6 +18,14 @@ func NewHexagon(box *geo.Box) Shape {
}
}
+func (s shapeHexagon) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ tl := s.Box.TopLeft.Copy()
+ tl.X += width / 4.
+ width /= 2.
+ return geo.NewBox(tl, width, s.Box.Height)
+}
+
func hexagonPath(box *geo.Box) *svg.SvgPathContext {
halfYFactor := 43.6 / 87.3
pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
@@ -38,5 +46,12 @@ func (s shapeHexagon) Perimeter() []geo.Intersectable {
func (s shapeHexagon) GetSVGPathData() []string {
return []string{
hexagonPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeHexagon) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := 2 * (width + 2*padding)
+ return totalWidth, height + 2*padding
+}
diff --git a/lib/shape/shape_oval.go b/lib/shape/shape_oval.go
index c0f983ad6..d29a23cc4 100644
--- a/lib/shape/shape_oval.go
+++ b/lib/shape/shape_oval.go
@@ -19,6 +19,16 @@ func NewOval(box *geo.Box) Shape {
}
}
+func (s shapeOval) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ height := s.Box.Height
+ insideTL := s.GetInsidePlacement(width, height, 0)
+ tl := s.Box.TopLeft.Copy()
+ width -= 2 * (insideTL.X - tl.X)
+ height -= 2 * (insideTL.Y - tl.Y)
+ return geo.NewBox(&insideTL, width, height)
+}
+
func (s shapeOval) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
theta := math.Atan2(height, width)
// add padding in direction of diagonal so there is padding distance between top left and border
@@ -53,3 +63,10 @@ func (s shapeOval) GetInsidePlacement(width, height, padding float64) geo.Point
func (s shapeOval) Perimeter() []geo.Intersectable {
return []geo.Intersectable{geo.NewEllipse(s.Box.Center(), s.Box.Width/2, s.Box.Height/2)}
}
+
+// debugging
+func (s shapeOval) GetSVGPathData() []string {
+ return []string{
+ boxPath(s.GetInnerBox()).PathData(),
+ }
+}
diff --git a/lib/shape/shape_package.go b/lib/shape/shape_package.go
index 8cdf73501..ba6e8c76c 100644
--- a/lib/shape/shape_package.go
+++ b/lib/shape/shape_package.go
@@ -11,6 +11,15 @@ type shapePackage struct {
*baseShape
}
+const (
+ packageTopMinHeight = 34.
+ packageTopMaxHeight = 55.
+ packageTopMinWidth = 50.
+ packageTopMaxWidth = 150.
+ packageHorizontalScalar = 0.5
+ packageVerticalScalar = 0.2
+)
+
func NewPackage(box *geo.Box) Shape {
return shapePackage{
baseShape: &baseShape{
@@ -20,22 +29,27 @@ func NewPackage(box *geo.Box) Shape {
}
}
-func packagePath(box *geo.Box) *svg.SvgPathContext {
- const MIN_TOP_HEIGHT = 34
- const MAX_TOP_HEIGHT = 55
- const MIN_TOP_WIDTH = 50
- const MAX_TOP_WIDTH = 150
+func (s shapePackage) GetInnerBox() *geo.Box {
+ tl := s.Box.TopLeft.Copy()
+ height := s.Box.Height
- const horizontalScalar = 0.5
- topWidth := box.Width * horizontalScalar
- if box.Width >= 2*MIN_TOP_WIDTH {
- topWidth = math.Min(MAX_TOP_WIDTH, math.Max(MIN_TOP_WIDTH, topWidth))
- }
- const verticalScalar = 0.2
- topHeight := box.Height * verticalScalar
- if box.Height >= 2*MIN_TOP_HEIGHT {
- topHeight = math.Min(MAX_TOP_HEIGHT, math.Max(MIN_TOP_HEIGHT, topHeight))
+ _, topHeight := getTopDimensions(s.Box)
+ tl.Y += topHeight
+ height -= topHeight
+ return geo.NewBox(tl, s.Box.Width, height)
+}
+
+func getTopDimensions(box *geo.Box) (width, height float64) {
+ width = box.Width * packageHorizontalScalar
+ if box.Width >= 2*packageTopMinWidth {
+ width = math.Min(packageTopMaxWidth, math.Max(packageTopMinWidth, width))
}
+ height = math.Min(packageTopMaxHeight, box.Height*packageVerticalScalar)
+ return width, height
+}
+
+func packagePath(box *geo.Box) *svg.SvgPathContext {
+ topWidth, topHeight := getTopDimensions(box)
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
pc.StartAt(pc.Absolute(0, 0))
@@ -55,5 +69,18 @@ func (s shapePackage) Perimeter() []geo.Intersectable {
func (s shapePackage) GetSVGPathData() []string {
return []string{
packagePath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapePackage) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ innerHeight := height + padding*2
+ // We want to compute what the topHeight will be to add to inner height;
+ // topHeight=(verticalScalar * totalHeight) and totalHeight=(topHeight + innerHeight)
+ // so solving for topHeight we get: topHeight=innerHeight * (verticalScalar/(1-verticalScalar))
+ topHeight := innerHeight * packageVerticalScalar / (1. - packageVerticalScalar)
+ totalHeight := innerHeight + math.Min(topHeight, packageTopMaxHeight)
+
+ return width + padding*2, totalHeight
+}
diff --git a/lib/shape/shape_page.go b/lib/shape/shape_page.go
index ca55f71ac..347897601 100644
--- a/lib/shape/shape_page.go
+++ b/lib/shape/shape_page.go
@@ -1,6 +1,8 @@
package shape
import (
+ "math"
+
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/svg"
)
@@ -9,6 +11,12 @@ type shapePage struct {
*baseShape
}
+const (
+ // TODO: cleanup
+ pageCornerWidth = 20.8164
+ pageCornerHeight = 20.348
+)
+
func NewPage(box *geo.Box) Shape {
return shapePage{
baseShape: &baseShape{
@@ -18,49 +26,51 @@ func NewPage(box *geo.Box) Shape {
}
}
-const PAGE_WIDTH = 66.
-const PAGE_HEIGHT = 79.
+func (s shapePage) GetInnerBox() *geo.Box {
+ // Note: for simplicity this assumes shape padding is greater than pageCornerSize
+ width := s.Box.Width
+ // consider right hand side occupied by corner for short pages
+ if s.Box.Height < 3*pageCornerHeight {
+ width -= pageCornerWidth
+ }
+ return geo.NewBox(s.Box.TopLeft.Copy(), width, s.Box.Height)
+}
func pageOuterPath(box *geo.Box) *svg.SvgPathContext {
+ // TODO: cleanup
pc := svg.NewSVGPathContext(box.TopLeft, 1., 1.)
- baseX := box.Width - PAGE_WIDTH
- baseY := box.Height - PAGE_HEIGHT
pc.StartAt(pc.Absolute(0.5, 0))
- pc.H(false, baseX+45.1836) // = width-(66+45.1836)
- pc.C(false, baseX+46.3544, 0.0, baseX+47.479, 0.456297, baseX+48.3189, 1.27202)
- pc.L(false, baseX+64.6353, 17.12)
- pc.C(false, baseX+65.5077, 17.9674, baseX+66., 19.1318, baseX+66., 20.348)
- // baseY is not needed above because the coordinates start at 0
- pc.V(false, baseY+78.5)
- pc.C(false, baseX+66.0, baseY+78.7761, baseX+65.7761, baseY+79.0, baseX+65.5, baseY+79.0)
+ pc.H(false, box.Width-20.8164)
+ pc.C(false, box.Width-19.6456, 0.0, box.Width-18.521, 0.456297, box.Width-17.6811, 1.27202)
+ pc.L(false, box.Width-1.3647, 17.12)
+ pc.C(false, box.Width-0.4923, 17.9674, box.Width, 19.1318, box.Width, 20.348)
+ pc.V(false, box.Height-0.5)
+ pc.C(false, box.Width, box.Height-0.2239, box.Width-0.2239, box.Height, box.Width-0.5, box.Height)
- pc.H(false, .499999)
- pc.C(false, 0.223857, baseY+79.0, 0.0, baseY+78.7761, 0.0, baseY+78.5)
+ pc.H(false, 0.499999)
+ pc.C(false, 0.223857, box.Height, 0, box.Height-0.2239, 0, box.Height-0.5)
pc.V(false, 0.499999)
- pc.C(false, 0.0, 0.223857, 0.223857, 0.0, 0.5, 0.0)
+ pc.C(false, 0, 0.223857, 0.223857, 0, 0.5, 0)
pc.Z()
return pc
}
func pageInnerPath(box *geo.Box) *svg.SvgPathContext {
- baseX := box.Width - PAGE_WIDTH
- baseY := box.Height - PAGE_HEIGHT
-
pc := svg.NewSVGPathContext(box.TopLeft, 1., 1.)
- pc.StartAt(pc.Absolute(baseX+64.91803, baseY+79.))
+ pc.StartAt(pc.Absolute(box.Width-1.08197, box.Height))
pc.H(false, 1.08196)
pc.C(true, -0.64918, 0, -1.08196, -0.43287, -1.08196, -1.08219)
pc.V(false, 1.08219)
pc.C(true, 0, -0.64931, 0.43278, -1.08219, 1.08196, -1.08219)
- pc.H(true, baseX+43.27868)
+ pc.H(true, box.Width-22.72132)
pc.C(true, 0.64918, 0, 1.08196, 0.43287, 1.08196, 1.08219)
pc.V(true, 17.09863)
pc.C(true, 0, 1.29863, 0.86557, 2.38082, 2.38032, 2.38082)
- pc.H(false, baseX+64.91803)
+ pc.H(false, box.Width-1.08197)
pc.C(true, .64918, 0, 1.08196, 0.43287, 1.08196, 1.08196)
- pc.V(false, baseY+77.91780)
- pc.C(false, baseX+64.99999, baseY+78.56712, baseX+65.56721, baseY+79, baseX+64.91803, baseY+79)
+ pc.V(false, box.Height-1.0822)
+ pc.C(false, box.Width-1.0, box.Height-0.43288, box.Width-0.43279, box.Height, box.Width-1.08197, box.Height)
pc.Z()
return pc
}
@@ -73,5 +83,19 @@ func (s shapePage) GetSVGPathData() []string {
return []string{
pageOuterPath(s.Box).PathData(),
pageInnerPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapePage) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := width + padding*2
+ totalHeight := height + padding*2
+ // add space for corner with short pages
+ if totalHeight < 3*pageCornerHeight {
+ totalWidth += pageCornerWidth
+ }
+ totalWidth = math.Max(totalWidth, 2*pageCornerWidth)
+ totalHeight = math.Max(totalHeight, pageCornerHeight)
+ return totalWidth, totalHeight
+}
diff --git a/lib/shape/shape_parallelogram.go b/lib/shape/shape_parallelogram.go
index 3557579cb..d9b99df69 100644
--- a/lib/shape/shape_parallelogram.go
+++ b/lib/shape/shape_parallelogram.go
@@ -9,6 +9,8 @@ type shapeParallelogram struct {
*baseShape
}
+const parallelWedgeWidth = 26.
+
func NewParallelogram(box *geo.Box) Shape {
return shapeParallelogram{
baseShape: &baseShape{
@@ -18,8 +20,17 @@ func NewParallelogram(box *geo.Box) Shape {
}
}
+func (s shapeParallelogram) GetInnerBox() *geo.Box {
+ tl := s.Box.TopLeft.Copy()
+ width := s.Box.Width - 2*parallelWedgeWidth
+ tl.X += parallelWedgeWidth
+ return geo.NewBox(tl, width, s.Box.Height)
+}
+
func parallelogramPath(box *geo.Box) *svg.SvgPathContext {
- wedgeWidth := 26.0
+ wedgeWidth := parallelWedgeWidth
+ // Note: box width should always be larger than parallelWedgeWidth
+ // this just handles after collapsing into a line
if box.Width <= wedgeWidth {
wedgeWidth = box.Width / 2.0
}
@@ -40,5 +51,12 @@ func (s shapeParallelogram) Perimeter() []geo.Intersectable {
func (s shapeParallelogram) GetSVGPathData() []string {
return []string{
parallelogramPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeParallelogram) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := width + padding*2 + parallelWedgeWidth*2
+ return totalWidth, height + padding*2
+}
diff --git a/lib/shape/shape_person.go b/lib/shape/shape_person.go
index f75c6c49d..9cb4e19c9 100644
--- a/lib/shape/shape_person.go
+++ b/lib/shape/shape_person.go
@@ -18,6 +18,19 @@ func NewPerson(box *geo.Box) Shape {
}
}
+const (
+ personShoulderWidthFactor = 20.2 / 68.3
+)
+
+func (s shapePerson) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ tl := s.Box.TopLeft.Copy()
+ shoulderWidth := personShoulderWidthFactor * width
+ tl.X += shoulderWidth
+ width -= shoulderWidth * 2
+ return geo.NewBox(tl, width, s.Box.Height)
+}
+
func personPath(box *geo.Box) *svg.SvgPathContext {
pc := svg.NewSVGPathContext(box.TopLeft, box.Width/68.3, box.Height/77.4)
@@ -50,5 +63,16 @@ func (s shapePerson) Perimeter() []geo.Intersectable {
func (s shapePerson) GetSVGPathData() []string {
return []string{
personPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapePerson) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := width + padding*2
+ // see shapePackage
+ shoulderWidth := totalWidth * personShoulderWidthFactor / (1 - 2*personShoulderWidthFactor)
+ totalWidth += 2 * shoulderWidth
+ totalHeight := height + padding*2
+ return totalWidth, totalHeight
+}
diff --git a/lib/shape/shape_queue.go b/lib/shape/shape_queue.go
index 5f40893d5..8981adc31 100644
--- a/lib/shape/shape_queue.go
+++ b/lib/shape/shape_queue.go
@@ -18,46 +18,47 @@ func NewQueue(box *geo.Box) Shape {
}
}
+func getArcWidth(box *geo.Box) float64 {
+ arcWidth := defaultArcDepth
+ // Note: box width should always be larger than 3*default
+ // this just handles after collaping into an oval
+ if box.Width < arcWidth*2 {
+ arcWidth = box.Width / 2.0
+ }
+ return arcWidth
+}
+
func (s shapeQueue) GetInnerBox() *geo.Box {
width := s.Box.Width
tl := s.Box.TopLeft.Copy()
- arcDepth := 24.0
- if width < arcDepth*2 {
- arcDepth = width / 2.0
- }
- width -= 3 * arcDepth
- tl.X += arcDepth
+ arcWidth := getArcWidth(s.Box)
+ width -= 3 * arcWidth
+ tl.X += arcWidth
return geo.NewBox(tl, width, s.Box.Height)
}
func queueOuterPath(box *geo.Box) *svg.SvgPathContext {
- arcDepth := 24.0
+ arcWidth := getArcWidth(box)
multiplier := 0.45
- if box.Width < arcDepth*2 {
- arcDepth = box.Width / 2.0
- }
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
- pc.StartAt(pc.Absolute(arcDepth, 0))
- pc.H(true, box.Width-2*arcDepth)
+ pc.StartAt(pc.Absolute(arcWidth, 0))
+ pc.H(true, box.Width-2*arcWidth)
pc.C(false, box.Width, 0, box.Width, box.Height*multiplier, box.Width, box.Height/2.0)
- pc.C(false, box.Width, box.Height-box.Height*multiplier, box.Width, box.Height, box.Width-arcDepth, box.Height)
- pc.H(true, -1*(box.Width-2*arcDepth))
+ pc.C(false, box.Width, box.Height-box.Height*multiplier, box.Width, box.Height, box.Width-arcWidth, box.Height)
+ pc.H(true, -1*(box.Width-2*arcWidth))
pc.C(false, 0, box.Height, 0, box.Height-box.Height*multiplier, 0, box.Height/2.0)
- pc.C(false, 0, box.Height*multiplier, 0, 0, arcDepth, 0)
+ pc.C(false, 0, box.Height*multiplier, 0, 0, arcWidth, 0)
pc.Z()
return pc
}
func queueInnerPath(box *geo.Box) *svg.SvgPathContext {
- arcDepth := 24.0
+ arcWidth := getArcWidth(box)
multiplier := 0.45
- if box.Width < arcDepth*2 {
- arcDepth = box.Width / 2.0
- }
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
- pc.StartAt(pc.Absolute(box.Width-arcDepth, 0))
- pc.C(false, box.Width-2*arcDepth, 0, box.Width-2*arcDepth, box.Height*multiplier, box.Width-2*arcDepth, box.Height/2.0)
- pc.C(false, box.Width-2*arcDepth, box.Height-box.Height*multiplier, box.Width-2*arcDepth, box.Height, box.Width-arcDepth, box.Height)
+ pc.StartAt(pc.Absolute(box.Width-arcWidth, 0))
+ pc.C(false, box.Width-2*arcWidth, 0, box.Width-2*arcWidth, box.Height*multiplier, box.Width-2*arcWidth, box.Height/2.0)
+ pc.C(false, box.Width-2*arcWidth, box.Height-box.Height*multiplier, box.Width-2*arcWidth, box.Height, box.Width-arcWidth, box.Height)
return pc
}
@@ -69,5 +70,13 @@ func (s shapeQueue) GetSVGPathData() []string {
return []string{
queueOuterPath(s.Box).PathData(),
queueInnerPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeQueue) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ // 1 arc left, width+ padding, 2 arcs right
+ totalWidth := 3*defaultArcDepth + width + padding*2
+ return totalWidth, height + padding*2
+}
diff --git a/lib/shape/shape_step.go b/lib/shape/shape_step.go
index bd63fb01a..5732b2ad4 100644
--- a/lib/shape/shape_step.go
+++ b/lib/shape/shape_step.go
@@ -20,6 +20,14 @@ func NewStep(box *geo.Box) Shape {
const STEP_WEDGE_WIDTH = 35.0
+func (s shapeStep) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ tl := s.Box.TopLeft.Copy()
+ width -= 2 * STEP_WEDGE_WIDTH
+ tl.X += STEP_WEDGE_WIDTH
+ return geo.NewBox(tl, width, s.Box.Height)
+}
+
func stepPath(box *geo.Box) *svg.SvgPathContext {
wedgeWidth := STEP_WEDGE_WIDTH
if box.Width <= wedgeWidth {
@@ -43,5 +51,12 @@ func (s shapeStep) Perimeter() []geo.Intersectable {
func (s shapeStep) GetSVGPathData() []string {
return []string{
stepPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeStep) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := width + padding*2 + 2*STEP_WEDGE_WIDTH
+ return totalWidth, height + padding*2
+}
diff --git a/lib/shape/shape_stored_data.go b/lib/shape/shape_stored_data.go
index 734037a75..6d015a854 100644
--- a/lib/shape/shape_stored_data.go
+++ b/lib/shape/shape_stored_data.go
@@ -9,6 +9,8 @@ type shapeStoredData struct {
*baseShape
}
+const storedDataWedgeWidth = 15.
+
func NewStoredData(box *geo.Box) Shape {
return shapeStoredData{
baseShape: &baseShape{
@@ -18,8 +20,16 @@ func NewStoredData(box *geo.Box) Shape {
}
}
+func (s shapeStoredData) GetInnerBox() *geo.Box {
+ width := s.Box.Width
+ tl := s.Box.TopLeft.Copy()
+ width -= 2 * storedDataWedgeWidth
+ tl.X += storedDataWedgeWidth
+ return geo.NewBox(tl, width, s.Box.Height)
+}
+
func storedDataPath(box *geo.Box) *svg.SvgPathContext {
- wedgeWidth := 15.0
+ wedgeWidth := storedDataWedgeWidth
multiplier := 0.27
if box.Width < wedgeWidth*2 {
wedgeWidth = box.Width / 2.0
@@ -43,5 +53,12 @@ func (s shapeStoredData) Perimeter() []geo.Intersectable {
func (s shapeStoredData) GetSVGPathData() []string {
return []string{
storedDataPath(s.Box).PathData(),
+ // debugging
+ boxPath(s.GetInnerBox()).PathData(),
}
}
+
+func (s shapeStoredData) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
+ totalWidth := width + padding*2 + 2*storedDataWedgeWidth
+ return totalWidth, height + padding*2
+}