From ca29119c602938ffec8ed1703fce1c172242ef4f Mon Sep 17 00:00:00 2001 From: Gavin Nishizawa Date: Mon, 17 Apr 2023 12:06:17 -0700 Subject: [PATCH] adjust label positioning for arrowhead --- d2renderers/d2svg/d2svg.go | 54 ++--------------------- d2target/d2target.go | 90 +++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 76 deletions(-) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 05aea7a55..0babbe089 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -20,8 +20,6 @@ import ( "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" - "oss.terrastruct.com/util-go/go2" - "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2latex" @@ -39,8 +37,7 @@ import ( ) const ( - DEFAULT_PADDING = 100 - MIN_ARROWHEAD_STROKE_WIDTH = 2 + DEFAULT_PADDING = 100 appendixIconRadius = 16 ) @@ -109,56 +106,13 @@ func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string { ))) } -func arrowheadDimensions(arrowhead d2target.Arrowhead, strokeWidth float64) (width, height float64) { - var baseWidth, baseHeight float64 - var widthMultiplier, heightMultiplier float64 - switch arrowhead { - case d2target.ArrowArrowhead: - baseWidth = 4 - baseHeight = 4 - widthMultiplier = 4 - heightMultiplier = 4 - case d2target.TriangleArrowhead: - baseWidth = 4 - baseHeight = 4 - widthMultiplier = 3 - heightMultiplier = 4 - case d2target.LineArrowhead: - widthMultiplier = 5 - heightMultiplier = 8 - case d2target.FilledDiamondArrowhead: - baseWidth = 11 - baseHeight = 7 - widthMultiplier = 5.5 - heightMultiplier = 3.5 - case d2target.DiamondArrowhead: - baseWidth = 11 - baseHeight = 9 - widthMultiplier = 5.5 - heightMultiplier = 4.5 - case d2target.FilledCircleArrowhead, d2target.CircleArrowhead: - baseWidth = 8 - baseHeight = 8 - widthMultiplier = 5 - heightMultiplier = 5 - case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired: - baseWidth = 9 - baseHeight = 9 - widthMultiplier = 4.5 - heightMultiplier = 4.5 - } - - clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth) - return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier -} - func arrowheadMarker(isTarget bool, id string, connection d2target.Connection) string { arrowhead := connection.DstArrow if !isTarget { arrowhead = connection.SrcArrow } strokeWidth := float64(connection.StrokeWidth) - width, height := arrowheadDimensions(arrowhead, strokeWidth) + width, height := arrowhead.Dimensions(strokeWidth) var path string switch arrowhead { @@ -621,11 +575,9 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co } if connection.SrcLabel != nil && connection.SrcLabel.Label != "" { - // TODO use arrowhead label dimensions https://github.com/terrastruct/d2/issues/183 fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel.Label, false)) } if connection.DstLabel != nil && connection.DstLabel.Label != "" { - // TODO use arrowhead label dimensions https://github.com/terrastruct/d2/issues/183 fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true)) } fmt.Fprintf(writer, ``) @@ -642,7 +594,7 @@ func renderArrowheadLabel(connection d2target.Connection, text string, isDst boo height = float64(connection.SrcLabel.LabelHeight) } - labelTL := connection.GetArrowHeadLabelPosition(isDst) + labelTL := connection.GetArrowheadLabelPosition(isDst) // svg text is positioned with the center of its baseline baselineCenter := geo.Point{ diff --git a/d2target/d2target.go b/d2target/d2target.go index a84592239..cb173cb2e 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -29,6 +29,9 @@ const ( BG_COLOR = color.N7 FG_COLOR = color.N1 + + MIN_ARROWHEAD_STROKE_WIDTH = 2 + ARROWHEAD_PADDING = 2. ) var BorderOffset = geo.NewVector(5, 5) @@ -233,14 +236,14 @@ func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) { y2 = go2.Max(y2, int(labelTL.Y)+connection.LabelHeight) } if connection.SrcLabel != nil && connection.SrcLabel.Label != "" { - labelTL := connection.GetArrowHeadLabelPosition(false) + labelTL := connection.GetArrowheadLabelPosition(false) x1 = go2.Min(x1, int(labelTL.X)) y1 = go2.Min(y1, int(labelTL.Y)) x2 = go2.Max(x2, int(labelTL.X)+connection.SrcLabel.LabelWidth) y2 = go2.Max(y2, int(labelTL.Y)+connection.SrcLabel.LabelHeight) } if connection.DstLabel != nil && connection.DstLabel.Label != "" { - labelTL := connection.GetArrowHeadLabelPosition(true) + labelTL := connection.GetArrowheadLabelPosition(true) x1 = go2.Min(x1, int(labelTL.X)) y1 = go2.Min(y1, int(labelTL.Y)) x2 = go2.Max(x2, int(labelTL.X)+connection.DstLabel.LabelWidth) @@ -534,7 +537,7 @@ func (c *Connection) GetLabelTopLeft() *geo.Point { return point } -func (connection *Connection) GetArrowHeadLabelPosition(isDst bool) *geo.Point { +func (connection *Connection) GetArrowheadLabelPosition(isDst bool) *geo.Point { var width, height float64 if isDst { width = float64(connection.DstLabel.LabelWidth) @@ -578,36 +581,30 @@ func (connection *Connection) GetArrowHeadLabelPosition(isDst bool) *geo.Point { } } - labelTL, index := label.UnlockedTop.GetPointOnRoute(connection.Route, float64(connection.StrokeWidth), position, width, height) + strokeWidth := float64(connection.StrokeWidth) - var arrowheadOffset float64 + labelTL, index := label.UnlockedTop.GetPointOnRoute(connection.Route, strokeWidth, position, width, height) + + var arrowSize float64 if isDst && connection.DstArrow != NoArrowhead { - // TODO offset according to arrowhead dimensions - arrowheadOffset = 5 + // Note: these dimensions are for rendering arrowheads on their side so we want the height + _, arrowSize = connection.DstArrow.Dimensions(strokeWidth) } else if connection.SrcArrow != NoArrowhead { - arrowheadOffset = 5 + _, arrowSize = connection.SrcArrow.Dimensions(strokeWidth) } - var offsetX, offsetY float64 - // get the start/end points of edge segment with arrowhead - start, end = connection.Route[index], connection.Route[index+1] - if start.Y == end.Y { - // shift up/down over horizontal segment - offsetY = arrowheadOffset - if end.Y < start.Y { - offsetY = -offsetY - } - } else if start.X == end.X { - // shift left/right across vertical segment - offsetX = arrowheadOffset - if end.X < start.X { - offsetX = -offsetX + if arrowSize > 0 { + // labelTL already accounts for strokeWidth and padding, we only want to shift further if the arrow is larger than this + offset := (arrowSize/2 + ARROWHEAD_PADDING) - strokeWidth/2 - label.PADDING + if offset > 0 { + start, end = connection.Route[index], connection.Route[index+1] + // Note: end to start to get normal towards unlocked top position + normalX, normalY := geo.GetUnitNormalVector(end.X, end.Y, start.X, start.Y) + labelTL.X += normalX * offset + labelTL.Y += normalY * offset } } - labelTL.X += offsetX - labelTL.Y += offsetY - return labelTL } @@ -681,6 +678,49 @@ func ToArrowhead(arrowheadType string, filled bool) Arrowhead { } } +func (arrowhead Arrowhead) Dimensions(strokeWidth float64) (width, height float64) { + var baseWidth, baseHeight float64 + var widthMultiplier, heightMultiplier float64 + switch arrowhead { + case ArrowArrowhead: + baseWidth = 4 + baseHeight = 4 + widthMultiplier = 4 + heightMultiplier = 4 + case TriangleArrowhead: + baseWidth = 4 + baseHeight = 4 + widthMultiplier = 3 + heightMultiplier = 4 + case LineArrowhead: + widthMultiplier = 5 + heightMultiplier = 8 + case FilledDiamondArrowhead: + baseWidth = 11 + baseHeight = 7 + widthMultiplier = 5.5 + heightMultiplier = 3.5 + case DiamondArrowhead: + baseWidth = 11 + baseHeight = 9 + widthMultiplier = 5.5 + heightMultiplier = 4.5 + case FilledCircleArrowhead, CircleArrowhead: + baseWidth = 8 + baseHeight = 8 + widthMultiplier = 5 + heightMultiplier = 5 + case CfOne, CfMany, CfOneRequired, CfManyRequired: + baseWidth = 9 + baseHeight = 9 + widthMultiplier = 4.5 + heightMultiplier = 4.5 + } + + clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth) + return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier +} + type Point struct { X int `json:"x"` Y int `json:"y"`