set up shape specific inner bounding boxes for labels

This commit is contained in:
Gavin Nishizawa 2023-01-20 20:04:59 -08:00
parent 9b0f942c05
commit 23097370e2
No known key found for this signature in database
GPG key ID: AE3B177777CE55CD
18 changed files with 427 additions and 127 deletions

View file

@ -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{}

View file

@ -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, `</g>`)
fmt.Fprintf(writer, closingTag)
fmt.Fprint(writer, `</g>`)
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, `</g>`)
fmt.Fprintf(writer, closingTag)
fmt.Fprint(writer, `</g>`)
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, `<path d="%s" style="%s"/>`, pathData, style)
}
case d2target.ShapeImage:
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<path d="%s" style="%s"/>`, pathData, style)
@ -864,7 +869,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
// Closes the class=shape
fmt.Fprintf(writer, `</g>`)
fmt.Fprint(writer, `</g>`)
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, `<rect class="shape" width="%d" height="%d" style="%s" />`,
targetShape.Width, targetShape.Height, containerStyle)
// Padding
fmt.Fprintf(writer, `<g transform="translate(6 6)">`)
fmt.Fprint(writer, `<g transform="translate(6 6)">`)
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, "</text>")
}
fmt.Fprintf(writer, "</g></g>")
fmt.Fprint(writer, "</g></g>")
} 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, `<g transform="translate(%f %f)">`, box.TopLeft.X, box.TopLeft.Y)
fmt.Fprint(writer, render)
fmt.Fprintf(writer, "</g>")
fmt.Fprint(writer, "</g>")
} 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, `<style type="text/css">%s</style>`, mdCSS)
}
if sketchRunner != nil {
fmt.Fprintf(buf, d2sketch.DefineFillPattern())
fmt.Fprint(buf, d2sketch.DefineFillPattern())
}
// only define shadow filter if a shape uses it

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}