feat: ability to add border-radius to d2 icons

This commit is contained in:
melsonic 2025-03-16 21:26:25 +05:30
parent 32c14d586c
commit 9603f1ae3b
No known key found for this signature in database
GPG key ID: DFA426742F621CD7
6 changed files with 67 additions and 14 deletions

View file

@ -383,7 +383,9 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
} else if isReserved {
c.compileReserved(&obj.Attributes, f)
return
if keyword != "icon" {
return
}
} else if f.Name.ScalarString() == "style" && f.Name.IsUnquoted() {
if f.Map() == nil || len(f.Map().Fields) == 0 {
c.errorf(f.LastRef().AST(), `"style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`)
@ -473,7 +475,10 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
}
attrs.Label.Value = scalar.ScalarString()
default:
attrs.Label.Value = scalar.ScalarString()
name := f.LastPrimaryKey().Key.Path[0].UnquotedString.Value[0].String
if *name != "icon" {
attrs.Label.Value = scalar.ScalarString()
}
}
attrs.Label.MapKey = f.LastPrimaryKey()
}
@ -757,7 +762,15 @@ func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
}
compileStyleFieldInit(attrs, f)
scalar := f.Primary().Value
err := attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString())
parentKeyword := f.LastPrimaryKey().Key.Path[0].ScalarString()
var err error
if parentKeyword == "icon" {
err = attrs.IconStyle.Apply(f.Name.ScalarString(), scalar.ScalarString())
} else {
err = attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString())
}
if err != nil {
c.errorf(scalar, err.Error())
return
@ -779,7 +792,12 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
case "stroke-dash":
attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "border-radius":
attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
if attrs.Style.BorderRadius == nil {
attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
}
if attrs.IconStyle.BorderRadius == nil {
attrs.IconStyle.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
}
case "shadow":
attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "3d":

View file

@ -194,6 +194,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
if obj.Style.DoubleBorder != nil {
shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
}
if obj.IconStyle.BorderRadius != nil {
shape.IconBorderRadius, _ = strconv.Atoi(obj.IconStyle.BorderRadius.Value)
}
}
func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {

View file

@ -196,10 +196,11 @@ type Attributes struct {
Label Scalar `json:"label"`
LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
Style Style `json:"style"`
Icon *url.URL `json:"icon,omitempty"`
Tooltip *Scalar `json:"tooltip,omitempty"`
Link *Scalar `json:"link,omitempty"`
Style Style `json:"style"`
Icon *url.URL `json:"icon,omitempty"`
IconStyle Style `json:"iconStyle"`
Tooltip *Scalar `json:"tooltip,omitempty"`
Link *Scalar `json:"link,omitempty"`
WidthAttr *Scalar `json:"width,omitempty"`
HeightAttr *Scalar `json:"height,omitempty"`

View file

@ -1163,6 +1163,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
}
case d2target.ShapeImage:
fmt.Fprint(writer, clipPathForIconBorderRadius(diagramHash, targetShape))
el := d2themes.NewThemableElement("image", inlineTheme)
el.X = float64(targetShape.Pos.X)
el.Y = float64(targetShape.Pos.Y)
@ -1172,6 +1173,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
el.Fill = fill
el.Stroke = stroke
el.Style = style
el.ClipPath = fmt.Sprintf("%v-%v-icon", diagramHash, targetShape.ID)
fmt.Fprint(writer, el.Render())
// TODO should standardize "" to rectangle
@ -1364,12 +1366,13 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d" />`,
fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d" clip-path="inset(0 round %dpx)" />`,
html.EscapeString(targetShape.Icon.String()),
tl.X,
tl.Y,
iconSize,
iconSize,
targetShape.IconBorderRadius,
)
}

View file

@ -41,6 +41,33 @@ func clipPathForBorderRadius(diagramHash string, shape d2target.Shape) string {
return out + `fill="none" /> </clipPath>`
}
func clipPathForIconBorderRadius(diagramHash string, shape d2target.Shape) string {
box := geo.NewBox(
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
float64(shape.Width),
float64(shape.Height),
)
topX, topY := box.TopLeft.X+box.Width, box.TopLeft.Y
out := fmt.Sprintf(`<clipPath id="%v-%v-icon">`, diagramHash, shape.ID)
out += fmt.Sprintf(`<path d="M %f %f L %f %f S %f %f %f %f `, box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius), box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius), box.TopLeft.X, box.TopLeft.Y, box.TopLeft.X+float64(shape.IconBorderRadius), box.TopLeft.Y)
out += fmt.Sprintf(`L %f %f L %f %f `, box.TopLeft.X+box.Width-float64(shape.IconBorderRadius), box.TopLeft.Y, topX-float64(shape.IconBorderRadius), topY)
out += fmt.Sprintf(`S %f %f %f %f `, topX, topY, topX, topY+float64(shape.IconBorderRadius))
out += fmt.Sprintf(`L %f %f `, topX, topY+box.Height-float64(shape.IconBorderRadius))
if len(shape.Columns) != 0 {
out += fmt.Sprintf(`L %f %f L %f %f`, topX, topY+box.Height, box.TopLeft.X, box.TopLeft.Y+box.Height)
} else {
out += fmt.Sprintf(`S %f % f %f %f `, topX, topY+box.Height, topX-float64(shape.IconBorderRadius), topY+box.Height)
out += fmt.Sprintf(`L %f %f `, box.TopLeft.X+float64(shape.IconBorderRadius), box.TopLeft.Y+box.Height)
out += fmt.Sprintf(`S %f %f %f %f`, box.TopLeft.X, box.TopLeft.Y+box.Height, box.TopLeft.X, box.TopLeft.Y+box.Height-float64(shape.IconBorderRadius))
out += fmt.Sprintf(`L %f %f`, box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius))
}
out += fmt.Sprintf(`Z %f %f" `, box.TopLeft.X, box.TopLeft.Y)
return out + `fill="none" /> </clipPath>`
}
func tableHeader(diagramHash string, shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64, inlineTheme *d2themes.Theme) string {
rectEl := d2themes.NewThemableElement("rect", inlineTheme)
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y

View file

@ -493,11 +493,12 @@ type Shape struct {
Multiple bool `json:"multiple"`
DoubleBorder bool `json:"double-border"`
Tooltip string `json:"tooltip"`
Link string `json:"link"`
PrettyLink string `json:"prettyLink,omitempty"`
Icon *url.URL `json:"icon"`
IconPosition string `json:"iconPosition"`
Tooltip string `json:"tooltip"`
Link string `json:"link"`
PrettyLink string `json:"prettyLink,omitempty"`
Icon *url.URL `json:"icon"`
IconBorderRadius int `json:"iconBorderRadius"`
IconPosition string `json:"iconPosition"`
// Whether the shape should allow shapes behind it to bleed through
// Currently just used for sequence diagram groups