set up shape specific inner bounding boxes for labels
This commit is contained in:
parent
9b0f942c05
commit
23097370e2
18 changed files with 427 additions and 127 deletions
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/d2themes"
|
"oss.terrastruct.com/d2/d2themes"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/shape"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1117,14 +1118,14 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
|
dslShape := strings.ToLower(obj.Attributes.Shape.Value)
|
||||||
|
|
||||||
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
|
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch shapeType {
|
switch dslShape {
|
||||||
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
|
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
|
||||||
// no labels
|
// no labels
|
||||||
default:
|
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.Width += INNER_LABEL_PADDING
|
||||||
labelDims.Height += 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()
|
paddingX, paddingY := obj.GetPadding()
|
||||||
|
|
||||||
switch shapeType {
|
switch dslShape {
|
||||||
case d2target.ShapeSquare, d2target.ShapeCircle:
|
case d2target.ShapeSquare, d2target.ShapeCircle:
|
||||||
if desiredWidth != 0 || desiredHeight != 0 {
|
if desiredWidth != 0 || desiredHeight != 0 {
|
||||||
paddingX = 0.
|
paddingX = 0.
|
||||||
|
|
@ -1169,6 +1170,12 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
|
||||||
obj.Height += float64(paddingY)
|
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 {
|
for _, edge := range g.Edges {
|
||||||
endpointLabels := []string{}
|
endpointLabels := []string{}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const (
|
||||||
appendixIconRadius = 16
|
appendixIconRadius = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
var multipleOffset = geo.NewVector(10, -10)
|
var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET)
|
||||||
|
|
||||||
//go:embed tooltip.svg
|
//go:embed tooltip.svg
|
||||||
var TooltipIcon string
|
var TooltipIcon string
|
||||||
|
|
@ -734,7 +734,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
|
|
||||||
var multipleTL *geo.Point
|
var multipleTL *geo.Point
|
||||||
if targetShape.Multiple {
|
if targetShape.Multiple {
|
||||||
multipleTL = tl.AddVector(geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET))
|
multipleTL = tl.AddVector(multipleOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch targetShape.Type {
|
switch targetShape.Type {
|
||||||
|
|
@ -744,13 +744,13 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
drawClass(writer, targetShape)
|
drawClass(writer, targetShape)
|
||||||
}
|
}
|
||||||
addAppendixItems(writer, targetShape)
|
addAppendixItems(writer, targetShape)
|
||||||
fmt.Fprintf(writer, `</g>`)
|
fmt.Fprint(writer, `</g>`)
|
||||||
fmt.Fprintf(writer, closingTag)
|
fmt.Fprint(writer, closingTag)
|
||||||
return labelMask, nil
|
return labelMask, nil
|
||||||
case d2target.ShapeSQLTable:
|
case d2target.ShapeSQLTable:
|
||||||
if sketchRunner != nil {
|
if sketchRunner != nil {
|
||||||
|
|
@ -758,13 +758,13 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
drawTable(writer, targetShape)
|
drawTable(writer, targetShape)
|
||||||
}
|
}
|
||||||
addAppendixItems(writer, targetShape)
|
addAppendixItems(writer, targetShape)
|
||||||
fmt.Fprintf(writer, `</g>`)
|
fmt.Fprint(writer, `</g>`)
|
||||||
fmt.Fprintf(writer, closingTag)
|
fmt.Fprint(writer, closingTag)
|
||||||
return labelMask, nil
|
return labelMask, nil
|
||||||
case d2target.ShapeOval:
|
case d2target.ShapeOval:
|
||||||
if targetShape.DoubleBorder {
|
if targetShape.DoubleBorder {
|
||||||
|
|
@ -776,7 +776,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(writer, renderDoubleOval(tl, width, height, style))
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(writer, renderOval(tl, width, height, style))
|
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:
|
case d2target.ShapeImage:
|
||||||
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
||||||
html.EscapeString(targetShape.Icon.String()),
|
html.EscapeString(targetShape.Icon.String()),
|
||||||
|
|
@ -815,7 +820,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
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)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
|
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)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
} else {
|
} else {
|
||||||
for _, pathData := range s.GetSVGPathData() {
|
for _, pathData := range s.GetSVGPathData() {
|
||||||
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, pathData, style)
|
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
|
// Closes the class=shape
|
||||||
fmt.Fprintf(writer, `</g>`)
|
fmt.Fprint(writer, `</g>`)
|
||||||
|
|
||||||
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
|
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
|
||||||
iconPosition := label.Position(targetShape.IconPosition)
|
iconPosition := label.Position(targetShape.IconPosition)
|
||||||
|
|
@ -895,7 +900,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
} else {
|
} else {
|
||||||
box = s.GetInnerBox()
|
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"
|
fontClass := "text"
|
||||||
if targetShape.Bold {
|
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" />`,
|
fmt.Fprintf(writer, `<rect class="shape" width="%d" height="%d" style="%s" />`,
|
||||||
targetShape.Width, targetShape.Height, containerStyle)
|
targetShape.Width, targetShape.Height, containerStyle)
|
||||||
// Padding
|
// 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()) {
|
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
|
// 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.Fprint(writer, "</text>")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(writer, "</g></g>")
|
fmt.Fprint(writer, "</g></g>")
|
||||||
} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
|
} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
|
||||||
render, err := d2latex.Render(targetShape.Label)
|
render, err := d2latex.Render(targetShape.Label)
|
||||||
if err != nil {
|
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.Fprintf(writer, `<g transform="translate(%f %f)">`, box.TopLeft.X, box.TopLeft.Y)
|
||||||
fmt.Fprint(writer, render)
|
fmt.Fprint(writer, render)
|
||||||
fmt.Fprintf(writer, "</g>")
|
fmt.Fprint(writer, "</g>")
|
||||||
} else if targetShape.Type == d2target.ShapeText && targetShape.Language != "" {
|
} else if targetShape.Type == d2target.ShapeText && targetShape.Language != "" {
|
||||||
render, err := textmeasure.RenderMarkdown(targetShape.Label)
|
render, err := textmeasure.RenderMarkdown(targetShape.Label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1000,7 +1009,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
|
||||||
|
|
||||||
addAppendixItems(writer, targetShape)
|
addAppendixItems(writer, targetShape)
|
||||||
|
|
||||||
fmt.Fprintf(writer, closingTag)
|
fmt.Fprint(writer, closingTag)
|
||||||
return labelMask, nil
|
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)
|
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, mdCSS)
|
||||||
}
|
}
|
||||||
if sketchRunner != nil {
|
if sketchRunner != nil {
|
||||||
fmt.Fprintf(buf, d2sketch.DefineFillPattern())
|
fmt.Fprint(buf, d2sketch.DefineFillPattern())
|
||||||
}
|
}
|
||||||
|
|
||||||
// only define shadow filter if a shape uses it
|
// only define shadow filter if a shape uses it
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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 *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) {
|
func (s baseShape) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
|
||||||
return width + padding*2, height + padding*2
|
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))
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ type shapeCallout struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTipWidth = 30.
|
||||||
|
defaultTipHeight = 45.
|
||||||
|
)
|
||||||
|
|
||||||
func NewCallout(box *geo.Box) Shape {
|
func NewCallout(box *geo.Box) Shape {
|
||||||
return shapeCallout{
|
return shapeCallout{
|
||||||
baseShape: &baseShape{
|
baseShape: &baseShape{
|
||||||
|
|
@ -18,25 +23,31 @@ func NewCallout(box *geo.Box) Shape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s shapeCallout) GetInnerBox() *geo.Box {
|
func getTipWidth(box *geo.Box) float64 {
|
||||||
height := s.Box.Height
|
tipWidth := defaultTipWidth
|
||||||
tipHeight := 45.0
|
if box.Width < tipWidth*2 {
|
||||||
if height < tipHeight*2 {
|
tipWidth = box.Width / 2.0
|
||||||
tipHeight = height / 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)
|
return geo.NewBox(s.Box.TopLeft.Copy(), s.Box.Width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calloutPath(box *geo.Box) *svg.SvgPathContext {
|
func calloutPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
tipWidth := 30.0
|
tipWidth := getTipWidth(box)
|
||||||
if box.Width < tipWidth*2 {
|
tipHeight := getTipHeight(box)
|
||||||
tipWidth = box.Width / 2.0
|
|
||||||
}
|
|
||||||
tipHeight := 45.0
|
|
||||||
if box.Height < tipHeight*2 {
|
|
||||||
tipHeight = box.Height / 2.0
|
|
||||||
}
|
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
||||||
pc.StartAt(pc.Absolute(0, 0))
|
pc.StartAt(pc.Absolute(0, 0))
|
||||||
pc.V(true, box.Height-tipHeight)
|
pc.V(true, box.Height-tipHeight)
|
||||||
|
|
@ -57,5 +68,19 @@ func (s shapeCallout) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeCallout) GetSVGPathData() []string {
|
func (s shapeCallout) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
calloutPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func (s shapeCircle) AspectRatio1() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s shapeCircle) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
|
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
|
diameter := math.Ceil(math.Sqrt(2 * math.Pow(math.Max(width, height)+2*padding, 2)))
|
||||||
return radius * 2, radius * 2
|
return diameter, diameter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s shapeCircle) GetInsidePlacement(width, height, padding float64) geo.Point {
|
func (s shapeCircle) GetInsidePlacement(width, height, padding float64) geo.Point {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func (s shapeCloud) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
|
||||||
width += padding
|
width += padding
|
||||||
height += padding
|
height += padding
|
||||||
|
|
@ -96,5 +114,7 @@ func (s shapeCloud) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeCloud) GetSVGPathData() []string {
|
func (s shapeCloud) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
cloudPath(s.Box).PathData(),
|
cloudPath(s.Box).PathData(),
|
||||||
|
// debugging
|
||||||
|
boxPath(s.GetInnerBox()).PathData(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ type shapeCylinder struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultArcDepth = 24.
|
||||||
|
)
|
||||||
|
|
||||||
func NewCylinder(box *geo.Box) Shape {
|
func NewCylinder(box *geo.Box) Shape {
|
||||||
return shapeCylinder{
|
return shapeCylinder{
|
||||||
baseShape: &baseShape{
|
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 {
|
func (s shapeCylinder) GetInnerBox() *geo.Box {
|
||||||
height := s.Box.Height
|
height := s.Box.Height
|
||||||
tl := s.Box.TopLeft.Copy()
|
tl := s.Box.TopLeft.Copy()
|
||||||
arcDepth := 24.0
|
arc := getArcHeight(s.Box)
|
||||||
if height < arcDepth*2 {
|
height -= 3 * arc
|
||||||
arcDepth = height / 2.0
|
tl.Y += 2 * arc
|
||||||
}
|
|
||||||
height -= 3 * arcDepth
|
|
||||||
tl.Y += 2 * arcDepth
|
|
||||||
return geo.NewBox(tl, s.Box.Width, height)
|
return geo.NewBox(tl, s.Box.Width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cylinderOuterPath(box *geo.Box) *svg.SvgPathContext {
|
func cylinderOuterPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
arcDepth := 24.0
|
arcHeight := getArcHeight(box)
|
||||||
if box.Height < arcDepth*2 {
|
|
||||||
arcDepth = box.Height / 2
|
|
||||||
}
|
|
||||||
multiplier := 0.45
|
multiplier := 0.45
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
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, 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.C(false, box.Width-box.Width*multiplier, 0, box.Width, 0, box.Width, arcHeight)
|
||||||
pc.V(true, box.Height-arcDepth*2)
|
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, 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.C(false, box.Width*multiplier, box.Height, 0, box.Height, 0, box.Height-arcHeight)
|
||||||
pc.V(true, -(box.Height - arcDepth*2))
|
pc.V(true, -(box.Height - arcHeight*2))
|
||||||
pc.Z()
|
pc.Z()
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
||||||
func cylinderInnerPath(box *geo.Box) *svg.SvgPathContext {
|
func cylinderInnerPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
arcDepth := 24.0
|
arcHeight := getArcHeight(box)
|
||||||
if box.Height < arcDepth*2 {
|
|
||||||
arcDepth = box.Height / 2
|
|
||||||
}
|
|
||||||
multiplier := 0.45
|
multiplier := 0.45
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
||||||
pc.StartAt(pc.Absolute(0, arcDepth))
|
pc.StartAt(pc.Absolute(0, arcHeight))
|
||||||
pc.C(false, 0, arcDepth*2, box.Width*multiplier, arcDepth*2, box.Width/2, arcDepth*2)
|
pc.C(false, 0, arcHeight*2, box.Width*multiplier, arcHeight*2, box.Width/2, arcHeight*2)
|
||||||
pc.C(false, box.Width-box.Width*multiplier, arcDepth*2, box.Width, arcDepth*2, box.Width, arcDepth)
|
pc.C(false, box.Width-box.Width*multiplier, arcHeight*2, box.Width, arcHeight*2, box.Width, arcHeight)
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,5 +74,13 @@ func (s shapeCylinder) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
cylinderOuterPath(s.Box).PathData(),
|
cylinderOuterPath(s.Box).PathData(),
|
||||||
cylinderInnerPath(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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func diamondPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, box.Width/77, box.Height/76.9)
|
pc := svg.NewSVGPathContext(box.TopLeft, box.Width/77, box.Height/76.9)
|
||||||
pc.StartAt(pc.Absolute(38.5, 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 {
|
func (s shapeDiamond) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
diamondPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ type shapeDocument struct {
|
||||||
*baseShape
|
*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 {
|
func NewDocument(box *geo.Box) Shape {
|
||||||
return shapeDocument{
|
return shapeDocument{
|
||||||
baseShape: &baseShape{
|
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 {
|
func documentPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
pathHeight := 18.925
|
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
|
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, 0, 0)
|
||||||
pc.L(false, 1, 0)
|
pc.L(false, 1, 0)
|
||||||
pc.L(false, 1, 16.3/pathHeight)
|
pc.L(false, 1, docPathBottom/docPathHeight)
|
||||||
pc.C(false, 5/6.0, 12.8/pathHeight, 2/3.0, 12.8/pathHeight, 1/2.0, 16.3/pathHeight)
|
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/pathHeight, 1/6.0, 19.8/pathHeight, 0, 16.3/pathHeight)
|
pc.C(false, 1/3.0, 19.8/docPathHeight, 1/6.0, 19.8/docPathHeight, 0, docPathBottom/docPathHeight)
|
||||||
pc.Z()
|
pc.Z()
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
@ -38,5 +49,12 @@ func (s shapeDocument) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeDocument) GetSVGPathData() []string {
|
func (s shapeDocument) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
documentPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func hexagonPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
halfYFactor := 43.6 / 87.3
|
halfYFactor := 43.6 / 87.3
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
|
pc := svg.NewSVGPathContext(box.TopLeft, box.Width, box.Height)
|
||||||
|
|
@ -38,5 +46,12 @@ func (s shapeHexagon) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeHexagon) GetSVGPathData() []string {
|
func (s shapeHexagon) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
hexagonPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func (s shapeOval) GetDimensionsToFit(width, height, padding float64) (float64, float64) {
|
||||||
theta := math.Atan2(height, width)
|
theta := math.Atan2(height, width)
|
||||||
// add padding in direction of diagonal so there is padding distance between top left and border
|
// 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 {
|
func (s shapeOval) Perimeter() []geo.Intersectable {
|
||||||
return []geo.Intersectable{geo.NewEllipse(s.Box.Center(), s.Box.Width/2, s.Box.Height/2)}
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ type shapePackage struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
packageTopMinHeight = 34.
|
||||||
|
packageTopMaxHeight = 55.
|
||||||
|
packageTopMinWidth = 50.
|
||||||
|
packageTopMaxWidth = 150.
|
||||||
|
packageHorizontalScalar = 0.5
|
||||||
|
packageVerticalScalar = 0.2
|
||||||
|
)
|
||||||
|
|
||||||
func NewPackage(box *geo.Box) Shape {
|
func NewPackage(box *geo.Box) Shape {
|
||||||
return shapePackage{
|
return shapePackage{
|
||||||
baseShape: &baseShape{
|
baseShape: &baseShape{
|
||||||
|
|
@ -20,22 +29,27 @@ func NewPackage(box *geo.Box) Shape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func packagePath(box *geo.Box) *svg.SvgPathContext {
|
func (s shapePackage) GetInnerBox() *geo.Box {
|
||||||
const MIN_TOP_HEIGHT = 34
|
tl := s.Box.TopLeft.Copy()
|
||||||
const MAX_TOP_HEIGHT = 55
|
height := s.Box.Height
|
||||||
const MIN_TOP_WIDTH = 50
|
|
||||||
const MAX_TOP_WIDTH = 150
|
|
||||||
|
|
||||||
const horizontalScalar = 0.5
|
_, topHeight := getTopDimensions(s.Box)
|
||||||
topWidth := box.Width * horizontalScalar
|
tl.Y += topHeight
|
||||||
if box.Width >= 2*MIN_TOP_WIDTH {
|
height -= topHeight
|
||||||
topWidth = math.Min(MAX_TOP_WIDTH, math.Max(MIN_TOP_WIDTH, topWidth))
|
return geo.NewBox(tl, s.Box.Width, height)
|
||||||
}
|
}
|
||||||
const verticalScalar = 0.2
|
|
||||||
topHeight := box.Height * verticalScalar
|
func getTopDimensions(box *geo.Box) (width, height float64) {
|
||||||
if box.Height >= 2*MIN_TOP_HEIGHT {
|
width = box.Width * packageHorizontalScalar
|
||||||
topHeight = math.Min(MAX_TOP_HEIGHT, math.Max(MIN_TOP_HEIGHT, topHeight))
|
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 := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
||||||
pc.StartAt(pc.Absolute(0, 0))
|
pc.StartAt(pc.Absolute(0, 0))
|
||||||
|
|
@ -55,5 +69,18 @@ func (s shapePackage) Perimeter() []geo.Intersectable {
|
||||||
func (s shapePackage) GetSVGPathData() []string {
|
func (s shapePackage) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
packagePath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package shape
|
package shape
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
"oss.terrastruct.com/d2/lib/svg"
|
"oss.terrastruct.com/d2/lib/svg"
|
||||||
)
|
)
|
||||||
|
|
@ -9,6 +11,12 @@ type shapePage struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TODO: cleanup
|
||||||
|
pageCornerWidth = 20.8164
|
||||||
|
pageCornerHeight = 20.348
|
||||||
|
)
|
||||||
|
|
||||||
func NewPage(box *geo.Box) Shape {
|
func NewPage(box *geo.Box) Shape {
|
||||||
return shapePage{
|
return shapePage{
|
||||||
baseShape: &baseShape{
|
baseShape: &baseShape{
|
||||||
|
|
@ -18,49 +26,51 @@ func NewPage(box *geo.Box) Shape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_WIDTH = 66.
|
func (s shapePage) GetInnerBox() *geo.Box {
|
||||||
const PAGE_HEIGHT = 79.
|
// 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 {
|
func pageOuterPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
|
// TODO: cleanup
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1., 1.)
|
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.StartAt(pc.Absolute(0.5, 0))
|
||||||
pc.H(false, baseX+45.1836) // = width-(66+45.1836)
|
pc.H(false, box.Width-20.8164)
|
||||||
pc.C(false, baseX+46.3544, 0.0, baseX+47.479, 0.456297, baseX+48.3189, 1.27202)
|
pc.C(false, box.Width-19.6456, 0.0, box.Width-18.521, 0.456297, box.Width-17.6811, 1.27202)
|
||||||
pc.L(false, baseX+64.6353, 17.12)
|
pc.L(false, box.Width-1.3647, 17.12)
|
||||||
pc.C(false, baseX+65.5077, 17.9674, baseX+66., 19.1318, baseX+66., 20.348)
|
pc.C(false, box.Width-0.4923, 17.9674, box.Width, 19.1318, box.Width, 20.348)
|
||||||
// baseY is not needed above because the coordinates start at 0
|
pc.V(false, box.Height-0.5)
|
||||||
pc.V(false, baseY+78.5)
|
pc.C(false, box.Width, box.Height-0.2239, box.Width-0.2239, box.Height, box.Width-0.5, box.Height)
|
||||||
pc.C(false, baseX+66.0, baseY+78.7761, baseX+65.7761, baseY+79.0, baseX+65.5, baseY+79.0)
|
|
||||||
|
|
||||||
pc.H(false, .499999)
|
pc.H(false, 0.499999)
|
||||||
pc.C(false, 0.223857, baseY+79.0, 0.0, baseY+78.7761, 0.0, baseY+78.5)
|
pc.C(false, 0.223857, box.Height, 0, box.Height-0.2239, 0, box.Height-0.5)
|
||||||
pc.V(false, 0.499999)
|
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()
|
pc.Z()
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
||||||
func pageInnerPath(box *geo.Box) *svg.SvgPathContext {
|
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 := 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.H(false, 1.08196)
|
||||||
pc.C(true, -0.64918, 0, -1.08196, -0.43287, -1.08196, -1.08219)
|
pc.C(true, -0.64918, 0, -1.08196, -0.43287, -1.08196, -1.08219)
|
||||||
pc.V(false, 1.08219)
|
pc.V(false, 1.08219)
|
||||||
pc.C(true, 0, -0.64931, 0.43278, -1.08219, 1.08196, -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.C(true, 0.64918, 0, 1.08196, 0.43287, 1.08196, 1.08219)
|
||||||
pc.V(true, 17.09863)
|
pc.V(true, 17.09863)
|
||||||
pc.C(true, 0, 1.29863, 0.86557, 2.38082, 2.38032, 2.38082)
|
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.C(true, .64918, 0, 1.08196, 0.43287, 1.08196, 1.08196)
|
||||||
pc.V(false, baseY+77.91780)
|
pc.V(false, box.Height-1.0822)
|
||||||
pc.C(false, baseX+64.99999, baseY+78.56712, baseX+65.56721, baseY+79, baseX+64.91803, baseY+79)
|
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()
|
pc.Z()
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
@ -73,5 +83,19 @@ func (s shapePage) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
pageOuterPath(s.Box).PathData(),
|
pageOuterPath(s.Box).PathData(),
|
||||||
pageInnerPath(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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ type shapeParallelogram struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parallelWedgeWidth = 26.
|
||||||
|
|
||||||
func NewParallelogram(box *geo.Box) Shape {
|
func NewParallelogram(box *geo.Box) Shape {
|
||||||
return shapeParallelogram{
|
return shapeParallelogram{
|
||||||
baseShape: &baseShape{
|
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 {
|
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 {
|
if box.Width <= wedgeWidth {
|
||||||
wedgeWidth = box.Width / 2.0
|
wedgeWidth = box.Width / 2.0
|
||||||
}
|
}
|
||||||
|
|
@ -40,5 +51,12 @@ func (s shapeParallelogram) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeParallelogram) GetSVGPathData() []string {
|
func (s shapeParallelogram) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
parallelogramPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func personPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, box.Width/68.3, box.Height/77.4)
|
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 {
|
func (s shapePerson) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
personPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func (s shapeQueue) GetInnerBox() *geo.Box {
|
||||||
width := s.Box.Width
|
width := s.Box.Width
|
||||||
tl := s.Box.TopLeft.Copy()
|
tl := s.Box.TopLeft.Copy()
|
||||||
arcDepth := 24.0
|
arcWidth := getArcWidth(s.Box)
|
||||||
if width < arcDepth*2 {
|
width -= 3 * arcWidth
|
||||||
arcDepth = width / 2.0
|
tl.X += arcWidth
|
||||||
}
|
|
||||||
width -= 3 * arcDepth
|
|
||||||
tl.X += arcDepth
|
|
||||||
return geo.NewBox(tl, width, s.Box.Height)
|
return geo.NewBox(tl, width, s.Box.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueOuterPath(box *geo.Box) *svg.SvgPathContext {
|
func queueOuterPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
arcDepth := 24.0
|
arcWidth := getArcWidth(box)
|
||||||
multiplier := 0.45
|
multiplier := 0.45
|
||||||
if box.Width < arcDepth*2 {
|
|
||||||
arcDepth = box.Width / 2.0
|
|
||||||
}
|
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
||||||
pc.StartAt(pc.Absolute(arcDepth, 0))
|
pc.StartAt(pc.Absolute(arcWidth, 0))
|
||||||
pc.H(true, box.Width-2*arcDepth)
|
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, 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.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*arcDepth))
|
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, 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()
|
pc.Z()
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueInnerPath(box *geo.Box) *svg.SvgPathContext {
|
func queueInnerPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
arcDepth := 24.0
|
arcWidth := getArcWidth(box)
|
||||||
multiplier := 0.45
|
multiplier := 0.45
|
||||||
if box.Width < arcDepth*2 {
|
|
||||||
arcDepth = box.Width / 2.0
|
|
||||||
}
|
|
||||||
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
||||||
pc.StartAt(pc.Absolute(box.Width-arcDepth, 0))
|
pc.StartAt(pc.Absolute(box.Width-arcWidth, 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*arcWidth, 0, box.Width-2*arcWidth, box.Height*multiplier, box.Width-2*arcWidth, 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.C(false, box.Width-2*arcWidth, box.Height-box.Height*multiplier, box.Width-2*arcWidth, box.Height, box.Width-arcWidth, box.Height)
|
||||||
return pc
|
return pc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,5 +70,13 @@ func (s shapeQueue) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
queueOuterPath(s.Box).PathData(),
|
queueOuterPath(s.Box).PathData(),
|
||||||
queueInnerPath(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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@ func NewStep(box *geo.Box) Shape {
|
||||||
|
|
||||||
const STEP_WEDGE_WIDTH = 35.0
|
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 {
|
func stepPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
wedgeWidth := STEP_WEDGE_WIDTH
|
wedgeWidth := STEP_WEDGE_WIDTH
|
||||||
if box.Width <= wedgeWidth {
|
if box.Width <= wedgeWidth {
|
||||||
|
|
@ -43,5 +51,12 @@ func (s shapeStep) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeStep) GetSVGPathData() []string {
|
func (s shapeStep) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
stepPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ type shapeStoredData struct {
|
||||||
*baseShape
|
*baseShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storedDataWedgeWidth = 15.
|
||||||
|
|
||||||
func NewStoredData(box *geo.Box) Shape {
|
func NewStoredData(box *geo.Box) Shape {
|
||||||
return shapeStoredData{
|
return shapeStoredData{
|
||||||
baseShape: &baseShape{
|
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 {
|
func storedDataPath(box *geo.Box) *svg.SvgPathContext {
|
||||||
wedgeWidth := 15.0
|
wedgeWidth := storedDataWedgeWidth
|
||||||
multiplier := 0.27
|
multiplier := 0.27
|
||||||
if box.Width < wedgeWidth*2 {
|
if box.Width < wedgeWidth*2 {
|
||||||
wedgeWidth = box.Width / 2.0
|
wedgeWidth = box.Width / 2.0
|
||||||
|
|
@ -43,5 +53,12 @@ func (s shapeStoredData) Perimeter() []geo.Intersectable {
|
||||||
func (s shapeStoredData) GetSVGPathData() []string {
|
func (s shapeStoredData) GetSVGPathData() []string {
|
||||||
return []string{
|
return []string{
|
||||||
storedDataPath(s.Box).PathData(),
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue