From 23097370e2c02a421e4da25679315cc156329521 Mon Sep 17 00:00:00 2001 From: Gavin Nishizawa Date: Fri, 20 Jan 2023 20:04:59 -0800 Subject: [PATCH] set up shape specific inner bounding boxes for labels --- d2graph/d2graph.go | 15 +++++-- d2renderers/d2svg/d2svg.go | 49 +++++++++++++---------- lib/shape/shape.go | 13 ++++++ lib/shape/shape_callout.go | 53 ++++++++++++++++++------- lib/shape/shape_circle.go | 14 ++++++- lib/shape/shape_cloud.go | 20 ++++++++++ lib/shape/shape_cylinder.go | 57 +++++++++++++++----------- lib/shape/shape_diamond.go | 19 +++++++++ lib/shape/shape_document.go | 28 ++++++++++--- lib/shape/shape_hexagon.go | 15 +++++++ lib/shape/shape_oval.go | 17 ++++++++ lib/shape/shape_package.go | 55 +++++++++++++++++++------- lib/shape/shape_page.go | 68 +++++++++++++++++++++----------- lib/shape/shape_parallelogram.go | 20 +++++++++- lib/shape/shape_person.go | 24 +++++++++++ lib/shape/shape_queue.go | 53 ++++++++++++++----------- lib/shape/shape_step.go | 15 +++++++ lib/shape/shape_stored_data.go | 19 ++++++++- 18 files changed, 427 insertions(+), 127 deletions(-) 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 +}