merge with master

This commit is contained in:
Antoine Poivey 2023-03-17 18:54:12 +01:00
commit 58b596854b
No known key found for this signature in database
GPG key ID: 6AA1C83421F1A287
577 changed files with 57669 additions and 15998 deletions

View file

@ -41,7 +41,7 @@ https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8
- [Community plugins](#community-plugins) - [Community plugins](#community-plugins)
- [Misc](#misc) - [Misc](#misc)
- [FAQ](#faq) - [FAQ](#faq)
- [Open-source projects documenting with D2](#open-source-projects-documenting-with-d2) - [Notable open-source projects documenting with D2](#notable-open-source-projects-documenting-with-d2)
## What does D2 look like? ## What does D2 look like?
@ -250,20 +250,24 @@ let us know and we'll be happy to include it here!
- I have a private inquiry. - I have a private inquiry.
- Please reach out at [hi@d2lang.com](hi@d2lang.com). - Please reach out at [hi@d2lang.com](hi@d2lang.com).
## Open-source projects documenting with D2 ## Notable open-source projects documenting with D2
Do you have or see an open-source project with `.d2` files? Please submit a PR adding to Do you have or see an open-source project with `.d2` files? Please submit a PR adding to
this list (ordered by star count, desc). this selected list of featured projects using D2.
- [Block Protocol](https://github.com/blockprotocol/blockprotocol) - The Block Protocol is - [ElasticSearch](https://github.com/elastic/beats/blob/main/libbeat/publisher/queue/proxy/diagrams/broker.d2)
an open standard for building and using data-driven blocks. - [UC
- [Ivy Wallet](https://github.com/Ivy-Apps/ivy-wallet) - Ivy Wallet is an open-source Berkeley](https://github.com/ucb-bar/hammer/blob/2b5c04d7b7d9ee3c73575efcd7ee0698bd5bfa88/doc/Hammer-Use/hier.d2)
money manager app for Android. - [Coronacheck](https://github.com/minvws/nl-covid19-coronacheck-app-ios/blob/e1567e9d1633b3273c537a105bff0e7d3a57ecfe/Diagrams/client-side-datamodel.d2)
- [Learn EVM Attacks](https://github.com/coinspect/learn-evm-attacks) - Learn & Contribute - Official app of the Netherlands for coronavirus entry passes.
on previously exploited vulnerabilities across several EVM projects. - [Block
- [BYCEPS](https://github.com/byceps/byceps) - BYCEPS is a self-hosted web platform to run Protocol](https://github.com/blockprotocol/blockprotocol/blob/db4cf8d422b881e52113aa52467d53115270e2b3/libs/%40blockprotocol/type-system/crate/assets/overview.d2)
LAN parties. - The Block Protocol is an open standard for building and using data-driven blocks.
- [Re:Earth](https://github.com/reearth/reearth-web) - A free, open and highly extensible - [Dagger](https://github.com/dagger/dagger/tree/main/cmd/dagger-graph) - A programmable
WebGIS platform. CI/CD engine that runs your pipelines in containers
- [Terraform OCI VSCode Server](https://github.com/timoa/terraform-oci-vscode-server) - - [Ivy
Terraform project that deploys VSCode Server on Oracle Cloud Infrastructure. Wallet](https://github.com/Ivy-Apps/ivy-wallet/blob/8062624bfa65175ec143cdc4038de27a84d38b57/assets/calc_algo.d2)
- Ivy Wallet is an open-source money manager app for Android.
- [Shed
Skin](https://github.com/shedskin/shedskin/blob/c7929e5fe0290d734ffb7e34e4cfc2cf731c7f98/docs/assets/diagrams/shedskin.d2)
- Python to C++ compiler

View file

@ -3,5 +3,3 @@
#### Improvements 🧹 #### Improvements 🧹
#### Bugfixes ⛑️ #### Bugfixes ⛑️
- Fixes `d2` erroring on malformed user paths (`fdopendir` error). [util-go#10](https://github.com/terrastruct/util-go/pull/10)

View file

@ -0,0 +1,28 @@
Customizations and layouts take a big leap forward with this release! Put together, these improvements make beautiful diagrams like these possible:
![mono](https://user-images.githubusercontent.com/3120367/225767298-73b6466c-c245-4df9-b9fd-9e2e4c7910c2.png)
> [Playground link](https://play.d2lang.com/?script=rJLRasMwDEXf_RX6gYWNvnmwXxkiEampaxlLXVdK_n3IidekHfRlT8GxonvvuUmkZy4HD1cH0FOMoHymMp8BBJViDErS3gDUhB5EudDwOaBiuzBHXePlQcuJ6tXk6kMLJjkGK9DdbYeXj_W1B6E0_ONMdcDJYkPmohhbnlPwcF0i7ekbR05T-8CyQS7ckwjfmCgXHOkBSH-Jwdg-p7Gsv-HuVp4twla4-1XMe04EkUdxk3MnaUUtDjIV4eQAarUe3navbc62Ll1365qPeCDoMcaHqQ2tzjBhb35mwRpu9at62JkU5gBC5evZqiFIjni5m7dgc4og6uZT6ybjSO9_Qp2ca0JbbrbyJuB-AgAA__8%3D&theme=300&sketch=0&layout=elk&)
#### Features 🚀
- New class of special themes, starting with `Terminal`, and `Terminal Grayscale`. See [docs](https://d2lang.com/tour/themes/#special-themes). [#1040](https://github.com/terrastruct/d2/pull/1040), [#1041](https://github.com/terrastruct/d2/pull/1041)
- `style.font: mono` to use a monospaced font for the text/label. See [docs](https://d2lang.com/tour/style/#font). [#1010](https://github.com/terrastruct/d2/pull/1010)
- `border-radius` is supported for both `class` and `sql_table` shapes. Thanks to second-time contributor @donglixiaoche ! [#982](https://github.com/terrastruct/d2/pull/982)
- Implements `style.fill-pattern`. See [docs](https://d2lang.com/tour/style#fill-pattern). [#1024](https://github.com/terrastruct/d2/pull/1024), [#1041](https://github.com/terrastruct/d2/pull/1041)
#### Improvements 🧹
- `dagre` layouts that have a connection where one endpoint is a container is much improved. [#1011](https://github.com/terrastruct/d2/pull/1011)
- `elk` layouts have less bends in the routes. [#1033](https://github.com/terrastruct/d2/pull/1033)
- `elk` layouts center nodes better. [#1028](https://github.com/terrastruct/d2/pull/1028)
- `elk` layouts have nicer margins between node boundaries and edges. [#1028](https://github.com/terrastruct/d2/pull/1028)
- `elk` layouts container contents are centered within. [#1038](https://github.com/terrastruct/d2/pull/1038)
- `elk` layouts container dimensions fit label. [#1038](https://github.com/terrastruct/d2/pull/1038)
- `sketch` draws connections with less roughness, which especially improves look of corner bends in ELK. [#1014](https://github.com/terrastruct/d2/pull/1014)
- CSS in SVGs are diagram-specific, which means you can embed multiple D2 diagrams on a web page without fear of style conflicts. [#1016](https://github.com/terrastruct/d2/pull/1016)
#### Bugfixes ⛑️
- Fixes `d2` erroring on malformed user paths (`fdopendir` error). [util-go#10](https://github.com/terrastruct/util-go/pull/10)
- Arrowhead labels being set without maps wasn't being picked up. [#1015](https://github.com/terrastruct/d2/pull/1015)
- Fixes a `dagre` layout error with connections to a container shape with a blockstring label. [#1032](https://github.com/terrastruct/d2/pull/1032)

View file

@ -159,9 +159,9 @@ func (r Range) Before(r2 Range) bool {
// //
// note: Line and Column are zero indexed. // note: Line and Column are zero indexed.
// note: Column and Byte are UTF-8 byte indexes unless byUTF16 was passed to Position.Advance in // note: Column and Byte are UTF-8 byte indexes unless byUTF16 was passed to Position.Advance in
// which they are UTF-16 code unit indexes. // . which they are UTF-16 code unit indexes.
// If intended for Javascript consumption like in the browser or via LSP, byUTF16 is // . If intended for Javascript consumption like in the browser or via LSP, byUTF16 is
// set to true. // . set to true.
type Position struct { type Position struct {
Line int Line int
Column int Column int
@ -650,10 +650,15 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
} }
if mk1.Value.Unbox() != nil { if mk1.Value.Unbox() != nil {
if (mk1.Value.ScalarBox().Unbox() == nil) != (mk2.Value.ScalarBox().Unbox() == nil) {
return false
}
if mk1.Value.ScalarBox().Unbox() != nil {
if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() { if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() {
return false return false
} }
} }
}
return true return true
} }

View file

@ -275,6 +275,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
opts := &d2lib.CompileOptions{ opts := &d2lib.CompileOptions{
Layout: layout, Layout: layout,
Ruler: ruler, Ruler: ruler,
ThemeID: themeID,
} }
if sketch { if sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn) opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
@ -432,6 +433,10 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
if err != nil { if err != nil {
return svg, err return svg, err
} }
out, err = png.AddExif(out)
if err != nil {
return svg, err
}
} else { } else {
if len(out) > 0 && out[len(out)-1] != '\n' { if len(out) > 0 && out[len(out)-1] != '\n' {
out = append(out, '\n') out = append(out, '\n')

View file

@ -392,6 +392,8 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "fill": case "fill":
attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "fill-pattern":
attrs.Style.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "stroke-width": case "stroke-width":
attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "stroke-dash": case "stroke-dash":
@ -488,11 +490,9 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
} }
if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" { if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
if f.Map() != nil {
c.compileArrowheads(edge, f) c.compileArrowheads(edge, f)
} }
} }
}
func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) { func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
var attrs *d2graph.Attributes var attrs *d2graph.Attributes
@ -508,6 +508,7 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
c.compileLabel(attrs, f) c.compileLabel(attrs, f)
} }
if f.Map() != nil {
for _, f2 := range f.Map().Fields { for _, f2 := range f.Map().Fields {
keyword := strings.ToLower(f2.Name) keyword := strings.ToLower(f2.Name)
_, isReserved := d2graph.SimpleReservedKeywords[keyword] _, isReserved := d2graph.SimpleReservedKeywords[keyword]
@ -526,6 +527,7 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
} }
} }
} }
}
// TODO add more, e.g. C, bash // TODO add more, e.g. C, bash
var ShortToFullLanguageAliases = map[string]string{ var ShortToFullLanguageAliases = map[string]string{

View file

@ -242,6 +242,25 @@ containers: {
} }
}, },
}, },
{
name: "fill-pattern",
text: `x: {
style: {
fill-pattern: dots
}
}
`,
},
{
name: "invalid-fill-pattern",
text: `x: {
style: {
fill-pattern: ddots
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain`,
},
{ {
name: "shape_unquoted_hex", name: "shape_unquoted_hex",
@ -971,6 +990,17 @@ x -> y: {
} }
}, },
}, },
{
name: "edge_arrowhead_primary",
text: `x -> y: {
source-arrowhead: Reisner's Rule of Conceptual Inertia
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
},
},
{ {
name: "edge_arrowhead_fields", name: "edge_arrowhead_fields",

View file

@ -9,6 +9,7 @@ import (
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/color"
) )
@ -20,22 +21,25 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
if fontFamily == nil { if fontFamily == nil {
fontFamily = go2.Pointer(d2fonts.SourceSansPro) fontFamily = go2.Pointer(d2fonts.SourceSansPro)
} }
if g.Theme != nil && g.Theme.SpecialRules.Mono {
fontFamily = go2.Pointer(d2fonts.SourceCodePro)
}
diagram.FontFamily = fontFamily diagram.FontFamily = fontFamily
diagram.Shapes = make([]d2target.Shape, len(g.Objects)) diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range g.Objects { for i := range g.Objects {
diagram.Shapes[i] = toShape(g.Objects[i]) diagram.Shapes[i] = toShape(g.Objects[i], g.Theme)
} }
diagram.Connections = make([]d2target.Connection, len(g.Edges)) diagram.Connections = make([]d2target.Connection, len(g.Edges))
for i := range g.Edges { for i := range g.Edges {
diagram.Connections[i] = toConnection(g.Edges[i]) diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
} }
return diagram, nil return diagram, nil
} }
func applyTheme(shape *d2target.Shape, obj *d2graph.Object) { func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
shape.Stroke = obj.GetStroke(shape.StrokeDash) shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill() shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText { if obj.Attributes.Shape.Value == d2target.ShapeText {
@ -46,6 +50,23 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
shape.SecondaryAccentColor = color.AA2 shape.SecondaryAccentColor = color.AA2
shape.NeutralAccentColor = color.N2 shape.NeutralAccentColor = color.N2
} }
// Theme options that change more than color
if theme != nil {
if theme.SpecialRules.OuterContainerDoubleBorder {
if obj.Level() == 1 && len(obj.ChildrenArray) > 0 {
shape.DoubleBorder = true
}
}
if theme.SpecialRules.ContainerDots {
if len(obj.ChildrenArray) > 0 {
shape.FillPattern = "dots"
}
}
if theme.SpecialRules.Mono {
shape.FontFamily = "mono"
}
}
} }
func applyStyles(shape *d2target.Shape, obj *d2graph.Object) { func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
@ -60,6 +81,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
} else if obj.Attributes.Shape.Value == d2target.ShapeText { } else if obj.Attributes.Shape.Value == d2target.ShapeText {
shape.Fill = "transparent" shape.Fill = "transparent"
} }
if obj.Attributes.Style.FillPattern != nil {
shape.FillPattern = obj.Attributes.Style.FillPattern.Value
}
if obj.Attributes.Style.Stroke != nil { if obj.Attributes.Style.Stroke != nil {
shape.Stroke = obj.Attributes.Style.Stroke.Value shape.Stroke = obj.Attributes.Style.Stroke.Value
} }
@ -99,7 +123,7 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
} }
} }
func toShape(obj *d2graph.Object) d2target.Shape { func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape() shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value) shape.SetType(obj.Attributes.Shape.Value)
shape.ID = obj.AbsID() shape.ID = obj.AbsID()
@ -124,7 +148,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
} }
applyStyles(shape, obj) applyStyles(shape, obj)
applyTheme(shape, obj) applyTheme(shape, obj, theme)
shape.Color = text.GetColor(shape.Italic) shape.Color = text.GetColor(shape.Italic)
applyStyles(shape, obj) applyStyles(shape, obj)
@ -165,7 +189,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
return *shape return *shape
} }
func toConnection(edge *d2graph.Edge) d2target.Connection { func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
connection := d2target.BaseConnection() connection := d2target.BaseConnection()
connection.ID = edge.AbsID() connection.ID = edge.AbsID()
connection.ZIndex = edge.ZIndex connection.ZIndex = edge.ZIndex
@ -205,7 +229,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
connection.DstLabel = edge.DstArrowhead.Label.Value connection.DstLabel = edge.DstArrowhead.Label.Value
} }
} }
if theme != nil && theme.SpecialRules.NoCornerRadius {
connection.BorderRadius = 0
}
if edge.Attributes.Style.BorderRadius != nil { if edge.Attributes.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64) connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64)
} }
@ -255,6 +281,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
if edge.Attributes.Style.Bold != nil { if edge.Attributes.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value) connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value)
} }
if theme != nil && theme.SpecialRules.Mono {
connection.FontFamily = "mono"
}
if edge.Attributes.Style.Font != nil { if edge.Attributes.Style.Font != nil {
connection.FontFamily = edge.Attributes.Style.Font.Value connection.FontFamily = edge.Attributes.Style.Font.Value
} }

View file

@ -609,6 +609,14 @@ y
x -> y x -> y
# foo # foo
y y
`,
},
{
name: "less_than_edge#955",
in: `
x <= y
`,
exp: `x <- = y
`, `,
}, },
} }

View file

@ -17,6 +17,8 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/shape"
@ -43,6 +45,8 @@ type Graph struct {
Layers []*Graph `json:"layers,omitempty"` Layers []*Graph `json:"layers,omitempty"`
Scenarios []*Graph `json:"scenarios,omitempty"` Scenarios []*Graph `json:"scenarios,omitempty"`
Steps []*Graph `json:"steps,omitempty"` Steps []*Graph `json:"steps,omitempty"`
Theme *d2themes.Theme `json:"theme,omitempty"`
} }
func NewGraph() *Graph { func NewGraph() *Graph {
@ -150,6 +154,7 @@ type Style struct {
Opacity *Scalar `json:"opacity,omitempty"` Opacity *Scalar `json:"opacity,omitempty"`
Stroke *Scalar `json:"stroke,omitempty"` Stroke *Scalar `json:"stroke,omitempty"`
Fill *Scalar `json:"fill,omitempty"` Fill *Scalar `json:"fill,omitempty"`
FillPattern *Scalar `json:"fillPattern,omitempty"`
StrokeWidth *Scalar `json:"strokeWidth,omitempty"` StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
StrokeDash *Scalar `json:"strokeDash,omitempty"` StrokeDash *Scalar `json:"strokeDash,omitempty"`
BorderRadius *Scalar `json:"borderRadius,omitempty"` BorderRadius *Scalar `json:"borderRadius,omitempty"`
@ -198,6 +203,14 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`) return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
} }
s.Fill.Value = value s.Fill.Value = value
case "fill-pattern":
if s.FillPattern == nil {
break
}
if !go2.Contains(FillPatterns, strings.ToLower(value)) {
return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(FillPatterns, ", "))
}
s.FillPattern.Value = value
case "stroke-width": case "stroke-width":
if s.StrokeWidth == nil { if s.StrokeWidth == nil {
break break
@ -259,10 +272,10 @@ func (s *Style) Apply(key, value string) error {
if s.Font == nil { if s.Font == nil {
break break
} }
if !go2.Contains(systemFonts, strings.ToUpper(value)) { if _, ok := d2fonts.D2_FONT_TO_FAMILY[strings.ToLower(value)]; !ok {
return fmt.Errorf(`"%v" is not a valid font in our system`, value) return fmt.Errorf(`"%v" is not a valid font in our system`, value)
} }
s.Font.Value = strings.ToUpper(value) s.Font.Value = strings.ToLower(value)
case "font-size": case "font-size":
if s.FontSize == nil { if s.FontSize == nil {
break break
@ -800,6 +813,11 @@ func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *
func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) { func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
shapeType := strings.ToLower(obj.Attributes.Shape.Value) shapeType := strings.ToLower(obj.Attributes.Shape.Value)
if obj.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[obj.Attributes.Style.Font.Value]
fontFamily = &f
}
var dims *d2target.TextDimensions var dims *d2target.TextDimensions
switch shapeType { switch shapeType {
case d2target.ShapeText: case d2target.ShapeText:
@ -1171,6 +1189,10 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
return d2target.NewTextDimensions(width, height), nil return d2target.NewTextDimensions(width, height), nil
} }
if strings.TrimSpace(t.Text) == "" {
return d2target.NewTextDimensions(1, 1), nil
}
return nil, fmt.Errorf("text not pre-measured and no ruler provided") return nil, fmt.Errorf("text not pre-measured and no ruler provided")
} }
@ -1240,6 +1262,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
} }
} }
if g.Theme != nil && g.Theme.SpecialRules.Mono {
tmp := d2fonts.SourceCodePro
fontFamily = &tmp
}
for _, obj := range g.Objects { for _, obj := range g.Objects {
obj.Box = &geo.Box{} obj.Box = &geo.Box{}
@ -1280,6 +1307,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue continue
} }
if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.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
@ -1397,7 +1428,16 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue continue
} }
dims := GetTextDimensions(mtexts, ruler, edge.Text(), fontFamily) if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value)
}
usedFont := fontFamily
if edge.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Attributes.Style.Font.Value]
usedFont = &f
}
dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
if dims == nil { if dims == nil {
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text()) return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
} }
@ -1412,9 +1452,15 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
func (g *Graph) Texts() []*d2target.MText { func (g *Graph) Texts() []*d2target.MText {
var texts []*d2target.MText var texts []*d2target.MText
capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
for _, obj := range g.Objects { for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" { if obj.Attributes.Label.Value != "" {
texts = appendTextDedup(texts, obj.Text()) text := obj.Text()
if capsLock {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
} }
if obj.Class != nil { if obj.Class != nil {
fontSize := d2fonts.FONT_SIZE_L fontSize := d2fonts.FONT_SIZE_L
@ -1441,7 +1487,11 @@ func (g *Graph) Texts() []*d2target.MText {
} }
for _, edge := range g.Edges { for _, edge := range g.Edges {
if edge.Attributes.Label.Value != "" { if edge.Attributes.Label.Value != "" {
texts = appendTextDedup(texts, edge.Text()) text := edge.Text()
if capsLock {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
} }
if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" { if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
t := edge.Text() t := edge.Text()
@ -1497,6 +1547,7 @@ var StyleKeywords = map[string]struct{}{
"opacity": {}, "opacity": {},
"stroke": {}, "stroke": {},
"fill": {}, "fill": {},
"fill-pattern": {},
"stroke-width": {}, "stroke-width": {},
"stroke-dash": {}, "stroke-dash": {},
"border-radius": {}, "border-radius": {},
@ -1538,6 +1589,12 @@ var NearConstantsArray = []string{
} }
var NearConstants map[string]struct{} var NearConstants map[string]struct{}
var FillPatterns = []string{
"dots",
"lines",
"grain",
}
// BoardKeywords contains the keywords that create new boards. // BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{ var BoardKeywords = map[string]struct{}{
"layers": {}, "layers": {},
@ -1633,3 +1690,15 @@ func (obj *Object) IsDescendantOf(ancestor *Object) bool {
} }
return obj.Parent.IsDescendantOf(ancestor) return obj.Parent.IsDescendantOf(ancestor)
} }
// ApplyTheme applies themes on the graph level
// This is different than on the render level, which only changes colors
// A theme applied on the graph level applies special rules that change the graph
func (g *Graph) ApplyTheme(themeID int64) error {
theme := d2themescatalog.Find(themeID)
if theme == (d2themes.Theme{}) {
return fmt.Errorf("theme %d not found", themeID)
}
g.Theme = &theme
return nil
}

View file

@ -1,10 +0,0 @@
package d2graph
var systemFonts = []string{
"DEFAULT",
"SERIOUS",
"DIGITAL",
"EDUCATIONAL",
"NEWSPAPER",
"MONO",
}

View file

@ -177,11 +177,27 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// we will chop the edge where it intersects the container border so it only shows the edge from the container // we will chop the edge where it intersects the container border so it only shows the edge from the container
src := edge.Src src := edge.Src
for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil { for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil {
src = src.ChildrenArray[0] // We want to get the bottom node of sources, setting its rank higher than all children
src = getLongestEdgeChainTail(g, src)
} }
dst := edge.Dst dst := edge.Dst
for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil { for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
dst = dst.ChildrenArray[0] dst = dst.ChildrenArray[0]
// We want to get the top node of destinations
for _, child := range dst.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, child) != nil && inContainer(e.Dst, dst) != nil {
isHead = false
break
}
}
if isHead {
dst = child
break
}
}
} }
if edge.SrcArrow && !edge.DstArrow { if edge.SrcArrow && !edge.DstArrow {
// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result // for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
@ -451,7 +467,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// if an edge to a container runs into its label, stop the edge at the label instead // if an edge to a container runs into its label, stop the edge at the label instead
overlapsContainerLabel := false overlapsContainerLabel := false
if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" { if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" && !dstShape.Is(shape.TEXT_TYPE) {
// assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label // assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label
labelWidth := float64(*edge.Dst.LabelWidth) labelWidth := float64(*edge.Dst.LabelWidth)
labelHeight := float64(*edge.Dst.LabelHeight) labelHeight := float64(*edge.Dst.LabelHeight)
@ -550,3 +566,66 @@ func generateAddParentLine(childID, parentID string) string {
func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string { func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string {
return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height) return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height)
} }
// getLongestEdgeChainTail gets the node at the end of the longest edge chain, because that will be the end of the container
// and is what external connections should connect with
func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
rank := make(map[*d2graph.Object]int)
for _, obj := range container.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
isHead = false
break
}
}
if !isHead {
continue
}
rank[obj] = 1
// BFS
queue := []*d2graph.Object{obj}
visited := make(map[*d2graph.Object]struct{})
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
if _, ok := visited[curr]; ok {
continue
}
visited[curr] = struct{}{}
for _, e := range g.Edges {
child := inContainer(e.Dst, container)
if child == curr {
continue
}
if child != nil && inContainer(e.Src, curr) != nil {
rank[child] = go2.Max(rank[child], rank[curr]+1)
queue = append(queue, child)
}
}
}
}
max := int(math.MinInt32)
var tail *d2graph.Object
for _, obj := range container.ChildrenArray {
if rank[obj] >= max {
max = rank[obj]
tail = obj
}
}
return tail
}
func inContainer(obj, container *d2graph.Object) *d2graph.Object {
if obj == nil {
return nil
}
if obj == container {
return obj
}
if obj.Parent == container {
return obj
}
return inContainer(obj.Parent, container)
}

View file

@ -8,6 +8,7 @@ import (
"context" "context"
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"strings" "strings"
@ -95,8 +96,11 @@ var DefaultOpts = ConfigurableOpts{
} }
var port_spacing = 40. var port_spacing = 40.
var edge_node_spacing = 40
type elkOpts struct { type elkOpts struct {
EdgeNode int `json:"elk.spacing.edgeNode,omitempty"`
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
Thoroughness int `json:"elk.layered.thoroughness,omitempty"` Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"` EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
Direction string `json:"elk.direction"` Direction string `json:"elk.direction"`
@ -106,6 +110,7 @@ type elkOpts struct {
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"` ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"` NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
ContentAlignment string `json:"elk.contentAlignment,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"` NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
ConfigurableOpts ConfigurableOpts
@ -140,8 +145,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
LayoutOptions: &elkOpts{ LayoutOptions: &elkOpts{
Thoroughness: 8, Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50, EdgeEdgeBetweenLayersSpacing: 50,
EdgeNode: edge_node_spacing,
HierarchyHandling: "INCLUDE_CHILDREN", HierarchyHandling: "INCLUDE_CHILDREN",
FixedAlignment: "BALANCED",
ConsiderModelOrder: "NODES_AND_EDGES", ConsiderModelOrder: "NODES_AND_EDGES",
NodeSizeConstraints: "MINIMUM_SIZE",
ContentAlignment: "H_CENTER V_CENTER",
ConfigurableOpts: ConfigurableOpts{ ConfigurableOpts: ConfigurableOpts{
Algorithm: opts.Algorithm, Algorithm: opts.Algorithm,
NodeSpacing: opts.NodeSpacing, NodeSpacing: opts.NodeSpacing,
@ -218,9 +227,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Thoroughness: 8, Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50, EdgeEdgeBetweenLayersSpacing: 50,
HierarchyHandling: "INCLUDE_CHILDREN", HierarchyHandling: "INCLUDE_CHILDREN",
FixedAlignment: "BALANCED",
EdgeNode: edge_node_spacing,
ConsiderModelOrder: "NODES_AND_EDGES", ConsiderModelOrder: "NODES_AND_EDGES",
// Why is it (height, width)? I have no clue, but it works. NodeSizeConstraints: "MINIMUM_SIZE",
NodeSizeMinimum: fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width))), ContentAlignment: "H_CENTER V_CENTER",
ConfigurableOpts: ConfigurableOpts{ ConfigurableOpts: ConfigurableOpts{
NodeSpacing: opts.NodeSpacing, NodeSpacing: opts.NodeSpacing,
EdgeNodeSpacing: opts.EdgeNodeSpacing, EdgeNodeSpacing: opts.EdgeNodeSpacing,
@ -228,11 +239,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Padding: opts.Padding, Padding: opts.Padding,
}, },
} }
// Only set if specified.
// There's a bug where if it's the node label dimensions that set the NodeSizeMinimum, switch elkGraph.LayoutOptions.Direction {
// then suddenly it's reversed back to (width, height). I must be missing something case "DOWN", "UP":
if obj.Attributes.Width != nil || obj.Attributes.Height != nil { n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
n.LayoutOptions.NodeSizeConstraints = "MINIMUM_SIZE" case "RIGHT", "LEFT":
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
} }
if n.LayoutOptions.Padding == DefaultOpts.Padding { if n.LayoutOptions.Padding == DefaultOpts.Padding {
@ -252,6 +264,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
paddingTop, paddingTop,
) )
} }
} else {
n.LayoutOptions = &elkOpts{
// Margins: "[top=100,left=100,bottom=100,right=100]",
}
} }
if obj.LabelWidth != nil && obj.LabelHeight != nil { if obj.LabelWidth != nil && obj.LabelHeight != nil {
@ -303,7 +319,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
val, err := vm.RunString(`elk.layout(graph) val, err := vm.RunString(`elk.layout(graph)
.then(s => s) .then(s => s)
.catch(s => s) .catch(err => err.message)
`) `)
if err != nil { if err != nil {
@ -324,7 +340,21 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
continue continue
} }
jsonOut := promise.Result().Export().(map[string]interface{}) if promise.State() == goja.PromiseStateRejected {
return errors.New("ELK: something went wrong")
}
result := promise.Result().Export()
var jsonOut map[string]interface{}
switch out := result.(type) {
case string:
return fmt.Errorf("ELK layout error: %s", out)
case map[string]interface{}:
jsonOut = out
default:
return fmt.Errorf("ELK unexpected return: %v", out)
}
jsonBytes, err := json.Marshal(jsonOut) jsonBytes, err := json.Marshal(jsonOut)
if err != nil { if err != nil {
@ -417,5 +447,270 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
edge.Route = points edge.Route = points
} }
deleteBends(g)
return nil return nil
} }
// deleteBends is a shim for ELK to delete unnecessary bends
// see https://github.com/terrastruct/d2/issues/1030
func deleteBends(g *d2graph.Graph) {
// Get rid of S-shapes at the source and the target
// TODO there might be value in repeating this. removal of an S shape introducing another S shape that can still be removed
for _, isSource := range []bool{true, false} {
for ei, e := range g.Edges {
if len(e.Route) < 4 {
continue
}
if e.Src == e.Dst {
continue
}
var endpoint *d2graph.Object
var start *geo.Point
var corner *geo.Point
var end *geo.Point
if isSource {
start = e.Route[0]
corner = e.Route[1]
end = e.Route[2]
endpoint = e.Src
} else {
start = e.Route[len(e.Route)-1]
corner = e.Route[len(e.Route)-2]
end = e.Route[len(e.Route)-3]
endpoint = e.Dst
}
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
// Make sure it's still attached
if isHorizontal {
if end.Y <= endpoint.TopLeft.Y+10 {
continue
}
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
continue
}
} else {
if end.X <= endpoint.TopLeft.X+10 {
continue
}
if end.X >= endpoint.TopLeft.X+endpoint.Width-10 {
continue
}
}
var newStart *geo.Point
if isHorizontal {
newStart = geo.NewPoint(start.X, end.Y)
} else {
newStart = geo.NewPoint(end.X, start.Y)
}
endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Attributes.Shape.Value)], endpoint.Box)
newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
// Check that the new segment doesn't collide with anything new
oldSegment := geo.NewSegment(start, corner)
newSegment := geo.NewSegment(newStart, end)
oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldSegment)
newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newSegment)
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount, oldOverlapsCount, oldCloseOverlapsCount, oldTouchingCount := countEdgeIntersects(g, g.Edges[ei], *oldSegment)
newCrossingsCount, newOverlapsCount, newCloseOverlapsCount, newTouchingCount := countEdgeIntersects(g, g.Edges[ei], *newSegment)
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
// commit
if isSource {
g.Edges[ei].Route = append(
[]*geo.Point{newStart},
e.Route[3:]...,
)
} else {
g.Edges[ei].Route = append(
e.Route[:len(e.Route)-3],
newStart,
)
}
}
}
// Get rid of ladders
// ELK likes to do these for some reason
// . ┌─
// . ┌─┘
// . │
// We want to transform these into L-shapes
for ei, e := range g.Edges {
if len(e.Route) < 6 {
continue
}
if e.Src == e.Dst {
continue
}
for i := 1; i < len(e.Route)-3; i++ {
before := e.Route[i-1]
start := e.Route[i]
corner := e.Route[i+1]
end := e.Route[i+2]
after := e.Route[i+3]
// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
// These concern two segments
var newCorner *geo.Point
if math.Ceil(start.X) == math.Ceil(corner.X) {
newCorner = geo.NewPoint(end.X, start.Y)
// not ladder
if (end.X > start.X) != (start.X > before.X) {
continue
}
if (end.Y > start.Y) != (after.Y > end.Y) {
continue
}
} else {
newCorner = geo.NewPoint(start.X, end.Y)
if (end.Y > start.Y) != (start.Y > before.Y) {
continue
}
if (end.X > start.X) != (after.X > end.X) {
continue
}
}
oldS1 := geo.NewSegment(start, corner)
oldS2 := geo.NewSegment(corner, end)
newS1 := geo.NewSegment(start, newCorner)
newS2 := geo.NewSegment(newCorner, end)
// Check that the new segments doesn't collide with anything new
oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldS1) + countObjectIntersects(g, e.Src, e.Dst, *oldS2)
newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newS1) + countObjectIntersects(g, e.Src, e.Dst, *newS2)
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount1, oldOverlapsCount1, oldCloseOverlapsCount1, oldTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *oldS1)
oldCrossingsCount2, oldOverlapsCount2, oldCloseOverlapsCount2, oldTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *oldS2)
oldCrossingsCount := oldCrossingsCount1 + oldCrossingsCount2
oldOverlapsCount := oldOverlapsCount1 + oldOverlapsCount2
oldCloseOverlapsCount := oldCloseOverlapsCount1 + oldCloseOverlapsCount2
oldTouchingCount := oldTouchingCount1 + oldTouchingCount2
newCrossingsCount1, newOverlapsCount1, newCloseOverlapsCount1, newTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *newS1)
newCrossingsCount2, newOverlapsCount2, newCloseOverlapsCount2, newTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *newS2)
newCrossingsCount := newCrossingsCount1 + newCrossingsCount2
newOverlapsCount := newOverlapsCount1 + newOverlapsCount2
newCloseOverlapsCount := newCloseOverlapsCount1 + newCloseOverlapsCount2
newTouchingCount := newTouchingCount1 + newTouchingCount2
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
// commit
g.Edges[ei].Route = append(append(
e.Route[:i],
newCorner,
),
e.Route[i+3:]...,
)
break
}
}
}
func countObjectIntersects(g *d2graph.Graph, src, dst *d2graph.Object, s geo.Segment) int {
count := 0
for i, o := range g.Objects {
if g.Objects[i] == src || g.Objects[i] == dst {
continue
}
if o.Intersects(s, float64(edge_node_spacing)-1) {
count++
}
}
return count
}
// countEdgeIntersects counts both crossings AND getting too close to a parallel segment
func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (int, int, int, int) {
isHorizontal := math.Ceil(s.Start.Y) == math.Ceil(s.End.Y)
crossingsCount := 0
overlapsCount := 0
closeOverlapsCount := 0
touchingCount := 0
for i, e := range g.Edges {
if g.Edges[i] == sEdge {
continue
}
for i := 0; i < len(e.Route)-1; i++ {
otherS := geo.NewSegment(e.Route[i], e.Route[i+1])
otherIsHorizontal := math.Ceil(otherS.Start.Y) == math.Ceil(otherS.End.Y)
if isHorizontal == otherIsHorizontal {
if s.Overlaps(*otherS, !isHorizontal, 0.) {
if isHorizontal {
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
touchingCount++
}
}
}
} else {
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
touchingCount++
}
}
}
}
}
} else {
if s.Intersects(*otherS) {
crossingsCount++
}
}
}
}
return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount
}

View file

@ -355,13 +355,13 @@ func (sd *sequenceDiagram) placeActors() {
} }
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline // addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
// ┌──────────────┐ // . ┌──────────────┐
// │ actor │ // . │ actor │
// └──────┬───────┘ // . └──────┬───────┘
// // .
// │ lifeline // . │ lifeline
// // .
// // .
func (sd *sequenceDiagram) addLifelineEdges() { func (sd *sequenceDiagram) addLifelineEdges() {
endY := 0. endY := 0.
if len(sd.messages) > 0 { if len(sd.messages) > 0 {
@ -433,17 +433,17 @@ func (sd *sequenceDiagram) placeNotes() {
} }
// placeSpans places spans over the object lifeline // placeSpans places spans over the object lifeline
// ┌──────────┐ // . ┌──────────┐
// │ actor │ // . │ actor │
// └────┬─────┘ // . └────┬─────┘
// ┌─┴──┐ // . ┌─┴──┐
// │ │ // . │ │
// |span| // . |span|
// │ │ // . │ │
// └─┬──┘ // . └─┬──┘
// // .
// lifeline // . lifeline
// // .
func (sd *sequenceDiagram) placeSpans() { func (sd *sequenceDiagram) placeSpans() {
// quickly find the span center X // quickly find the span center X
rankToX := make(map[int]float64) rankToX := make(map[int]float64)

View file

@ -22,6 +22,7 @@ type CompileOptions struct {
MeasuredTexts []*d2target.MText MeasuredTexts []*d2target.MText
Ruler *textmeasure.Ruler Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error Layout func(context.Context, *d2graph.Graph) error
ThemeID int64
// FontFamily controls the font family used for all texts that are not the following: // FontFamily controls the font family used for all texts that are not the following:
// - code // - code
@ -51,6 +52,11 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
} }
func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) { func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) {
err := g.ApplyTheme(opts.ThemeID)
if err != nil {
return nil, err
}
if len(g.Objects) > 0 { if len(g.Objects) > 0 {
err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily) err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil { if err != nil {

View file

@ -919,8 +919,7 @@ func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d
return !isSpecial return !isSpecial
}) })
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass {
ref.MapKey.Value.Map = nil deleteFromMap(ref.Scope, ref.MapKey)
ref.MapKey.Primary = ref.MapKey.Value.ScalarBox()
} else if len(withoutSpecial) == 0 { } else if len(withoutSpecial) == 0 {
hoistRefChildren(g, key, ref) hoistRefChildren(g, key, ref)
deleteFromMap(ref.Scope, ref.MapKey) deleteFromMap(ref.Scope, ref.MapKey)

View file

@ -1858,6 +1858,32 @@ func TestMove(t *testing.T) {
assert.JSON(t, g.Objects[1].ID, "c") assert.JSON(t, g.Objects[1].ID, "c")
}, },
}, },
{
name: "duplicate",
text: `a: {
b: {
shape: cylinder
}
}
a: {
b: {
shape: cylinder
}
}
`,
key: `a.b`,
newKey: `b`,
exp: `a
a
b: {
shape: cylinder
}
`,
},
{ {
name: "rename_2", name: "rename_2",
@ -3399,6 +3425,44 @@ func TestDelete(t *testing.T) {
exp: `x.y.z -> y.b exp: `x.y.z -> y.b
`, `,
}, },
{
name: "table_refs",
text: `a: {
shape: sql_table
b
}
c: {
shape: sql_table
d
}
a.b
a.b -> c.d
`,
key: `a`,
exp: `c: {
shape: sql_table
d
}
c.d
`,
},
{
name: "class_refs",
text: `a: {
shape: class
b: int
}
a.b
`,
key: `a`,
exp: ``,
},
{ {
name: "edge_both_identical_childs", name: "edge_both_identical_childs",
@ -4740,7 +4804,6 @@ a -> b
shape: sql_table shape: sql_table
id: int {constraint: primary_key} id: int {constraint: primary_key}
} }
disks.id disks.id
AWS S3 Vancouver -> disks AWS S3 Vancouver -> disks

View file

@ -102,6 +102,7 @@ func ParseValue(value string) (d2ast.Value, error) {
// - streaming parser isn't really helpful. // - streaming parser isn't really helpful.
// - just read into a string even and decode runes forward/back as needed // - just read into a string even and decode runes forward/back as needed
// - the whole file essentially exists within the parser as the AST anyway... // - the whole file essentially exists within the parser as the AST anyway...
//
// TODO: ast struct that combines map & errors and pass that around // TODO: ast struct that combines map & errors and pass that around
type parser struct { type parser struct {
path string path string
@ -315,10 +316,12 @@ func (p *parser) commit() {
// //
// TODO: make each parse function read its delimiter and return nil if not as expected // TODO: make each parse function read its delimiter and return nil if not as expected
// TODO: lookahead *must* always be empty in between parse calls. you either commit or // TODO: lookahead *must* always be empty in between parse calls. you either commit or
//
// rewind in each function. if you don't, you pass a hint. // rewind in each function. if you don't, you pass a hint.
// //
// TODO: omg we don't need two buffers, just a single lookahead and an index... // TODO: omg we don't need two buffers, just a single lookahead and an index...
// TODO: get rid of lookaheadPos or at least never use directly. maybe rename to beforePeekPos? // TODO: get rid of lookaheadPos or at least never use directly. maybe rename to beforePeekPos?
//
// or better yet keep positions in the lookahead buffer. // or better yet keep positions in the lookahead buffer.
// ok so plan here is to get rid of lookaheadPos and add a rewindPos that stores // ok so plan here is to get rid of lookaheadPos and add a rewindPos that stores
// the pos to rewind to. // the pos to rewind to.

View file

@ -363,6 +363,20 @@ not part of block string
name: "edge_group_value", name: "edge_group_value",
text: ` text: `
q.(x -> y).z: (rawr) q.(x -> y).z: (rawr)
`,
},
{
name: "less_than_edge#955",
text: `
x <= y
`,
},
{
name: "merged_shapes_#322",
text: `
a-
b-
c-
`, `,
}, },
} }

View file

@ -211,3 +211,8 @@ func init() {
Style: FONT_STYLE_BOLD, Style: FONT_STYLE_BOLD,
}] = b }] = b
} }
var D2_FONT_TO_FAMILY = map[string]FontFamily{
"default": SourceSansPro,
"mono": SourceCodePro,
}

View file

@ -98,6 +98,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range paths { for _, p := range paths {
@ -145,6 +146,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range pathsBigRect { for _, p := range pathsBigRect {
@ -192,6 +194,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range paths { for _, p := range paths {
@ -242,6 +245,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range pathsBigCircle { for _, p := range pathsBigCircle {
@ -292,6 +296,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
} }
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range sketchPaths { for _, p := range sketchPaths {
@ -316,7 +321,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
} }
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) { func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
roughness := 1.0 roughness := 0.5
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness) js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
paths, err := computeRoughPathData(r, js) paths, err := computeRoughPathData(r, js)
if err != nil { if err != nil {
@ -357,6 +362,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range paths { for _, p := range paths {
@ -383,6 +389,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
pathEl = d2themes.NewThemableElement("path") pathEl = d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill = shape.Fill pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header" pathEl.ClassName = "class_header"
for _, p := range paths { for _, p := range paths {
pathEl.D = p pathEl.D = p
@ -462,6 +469,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
} }
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.Fill = shape.Fill pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
for _, p := range paths { for _, p := range paths {
pathEl.D = p pathEl.D = p
output += pathEl.Render() output += pathEl.Render()
@ -496,6 +504,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path") pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape) pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape" pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle() pathEl.Style = shape.CSSStyle()
for _, p := range paths { for _, p := range paths {
@ -523,6 +532,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
pathEl = d2themes.NewThemableElement("path") pathEl = d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y)) pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill = shape.Fill pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header" pathEl.ClassName = "class_header"
for _, p := range paths { for _, p := range paths {
pathEl.D = p pathEl.D = p
@ -576,6 +586,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
} }
pathEl = d2themes.NewThemableElement("path") pathEl = d2themes.NewThemableElement("path")
pathEl.Fill = shape.Fill pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header" pathEl.ClassName = "class_header"
for _, p := range paths { for _, p := range paths {
pathEl.D = p pathEl.D = p

View file

@ -18,9 +18,11 @@ import (
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
) )
@ -39,6 +41,15 @@ func TestSketch(t *testing.T) {
script: `winter.snow -> summer.sun script: `winter.snow -> summer.sun
`, `,
}, },
{
name: "elk corners",
engine: "elk",
script: `a -> b
b -> c
a -> c
c -> a
`,
},
{ {
name: "animated", name: "animated",
script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true } script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
@ -495,6 +506,54 @@ darker: {
style.font-color: "#fff" style.font-color: "#fff"
style.fill: "#000" style.fill: "#000"
} }
`,
},
{
name: "terminal",
themeID: d2themescatalog.Terminal.ID,
script: `network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: { shape: hexagon }
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: { shape: page; style.multiple: true }
network.data processor -> api server
`, `,
}, },
{ {
@ -1003,6 +1062,185 @@ normal: {
nested normal nested normal
} }
something something
`,
},
{
name: "class_and_sqlTable_border_radius",
script: `
a: {
shape: sql_table
id: int {constraint: primary_key}
disk: int {constraint: foreign_key}
json: jsonb {constraint: unique}
last_updated: timestamp with time zone
style: {
fill: red
border-radius: 0
}
}
b: {
shape: class
field: "[]string"
method(a uint64): (x, y int)
style: {
border-radius: 0
}
}
c: {
shape: class
style: {
border-radius: 0
}
}
d: {
shape: sql_table
style: {
border-radius: 0
}
}
`,
},
{
name: "dots-real",
script: `
NETWORK: {
style: {
stroke: black
fill-pattern: dots
double-border: true
fill: "#E7E9EE"
font: mono
}
CELL TOWER: {
style: {
stroke: black
fill-pattern: dots
fill: "#F5F6F9"
font: mono
}
satellites: SATELLITES {
shape: stored_data
style: {
font: mono
fill: white
stroke: black
multiple: true
}
}
transmitter: TRANSMITTER {
style: {
font: mono
fill: white
stroke: black
}
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
}
}
D2 Parser: {
style.fill-pattern: grain
shape: class
+reader: io.RuneReader
# Default visibility is + so no need to specify.
readerPos: d2ast.Position
# Private field.
-lookahead: "[]rune"
# Escape the # to prevent being parsed as comment
#lookaheadPos: d2ast.Position
# Or just wrap in quotes
"#peekn(n int)": (s string, eof bool)
+peek(): (r rune, eof bool)
rewind()
commit()
}
`,
},
{
name: "dots-3d",
script: `x: {style.3d: true; style.fill-pattern: dots}
y: {shape: hexagon; style.3d: true; style.fill-pattern: dots}
`,
},
{
name: "dots-multiple",
script: `
rectangle: {shape: "rectangle"; style.fill-pattern: dots; style.multiple: true}
square: {shape: "square"; style.fill-pattern: dots; style.multiple: true}
page: {shape: "page"; style.fill-pattern: dots; style.multiple: true}
parallelogram: {shape: "parallelogram"; style.fill-pattern: dots; style.multiple: true}
document: {shape: "document"; style.fill-pattern: dots; style.multiple: true}
cylinder: {shape: "cylinder"; style.fill-pattern: dots; style.multiple: true}
queue: {shape: "queue"; style.fill-pattern: dots; style.multiple: true}
package: {shape: "package"; style.fill-pattern: dots; style.multiple: true}
step: {shape: "step"; style.fill-pattern: dots; style.multiple: true}
callout: {shape: "callout"; style.fill-pattern: dots; style.multiple: true}
stored_data: {shape: "stored_data"; style.fill-pattern: dots; style.multiple: true}
person: {shape: "person"; style.fill-pattern: dots; style.multiple: true}
diamond: {shape: "diamond"; style.fill-pattern: dots; style.multiple: true}
oval: {shape: "oval"; style.fill-pattern: dots; style.multiple: true}
circle: {shape: "circle"; style.fill-pattern: dots; style.multiple: true}
hexagon: {shape: "hexagon"; style.fill-pattern: dots; style.multiple: true}
cloud: {shape: "cloud"; style.fill-pattern: dots; style.multiple: true}
rectangle -> square -> page
parallelogram -> document -> cylinder
queue -> package -> step
callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
`,
},
{
name: "dots-all",
script: `
rectangle: {shape: "rectangle"; style.fill-pattern: dots}
square: {shape: "square"; style.fill-pattern: dots}
page: {shape: "page"; style.fill-pattern: dots}
parallelogram: {shape: "parallelogram"; style.fill-pattern: dots}
document: {shape: "document"; style.fill-pattern: dots}
cylinder: {shape: "cylinder"; style.fill-pattern: dots}
queue: {shape: "queue"; style.fill-pattern: dots}
package: {shape: "package"; style.fill-pattern: dots}
step: {shape: "step"; style.fill-pattern: dots}
callout: {shape: "callout"; style.fill-pattern: dots}
stored_data: {shape: "stored_data"; style.fill-pattern: dots}
person: {shape: "person"; style.fill-pattern: dots}
diamond: {shape: "diamond"; style.fill-pattern: dots}
oval: {shape: "oval"; style.fill-pattern: dots}
circle: {shape: "circle"; style.fill-pattern: dots}
hexagon: {shape: "hexagon"; style.fill-pattern: dots}
cloud: {shape: "cloud"; style.fill-pattern: dots}
rectangle -> square -> page
parallelogram -> document -> cylinder
queue -> package -> step
callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
`, `,
}, },
} }
@ -1014,6 +1252,7 @@ type testCase struct {
themeID int64 themeID int64
script string script string
skip bool skip bool
engine string
} }
func runa(t *testing.T, tcs []testCase) { func runa(t *testing.T, tcs []testCase) {
@ -1040,10 +1279,15 @@ func run(t *testing.T, tc testCase) {
return return
} }
layout := d2dagrelayout.DefaultLayout
if strings.EqualFold(tc.engine, "elk") {
layout = d2elklayout.DefaultLayout
}
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout, Layout: layout,
FontFamily: go2.Pointer(d2fonts.HandDrawn), FontFamily: go2.Pointer(d2fonts.HandDrawn),
ThemeID: tc.themeID,
}) })
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 299 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 290 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 285 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 276 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 335 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 326 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 280 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 271 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 285 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 278 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 269 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 325 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 333 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 350 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 316 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 290 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 235 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 341 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 332 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 230 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 307 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 107 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 355 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 420 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 420 KiB

View file

@ -138,6 +138,7 @@ func run(t *testing.T, tc testCase) {
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout, Layout: d2dagrelayout.DefaultLayout,
ThemeID: tc.themeID,
}) })
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 806 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

View file

@ -11,12 +11,16 @@ import (
"oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/d2/lib/svg"
) )
func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { func classHeader(diagramHash string, shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
rectEl := d2themes.NewThemableElement("rect") rectEl := d2themes.NewThemableElement("rect")
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y
rectEl.Width, rectEl.Height = box.Width, box.Height rectEl.Width, rectEl.Height = box.Width, box.Height
rectEl.Fill = shape.Fill rectEl.Fill = shape.Fill
rectEl.FillPattern = shape.FillPattern
rectEl.ClassName = "class_header" rectEl.ClassName = "class_header"
if shape.BorderRadius != 0 {
rectEl.ClipPath = fmt.Sprintf("%v-%v", diagramHash, shape.ID)
}
str := rectEl.Render() str := rectEl.Render()
if text != "" { if text != "" {
@ -81,14 +85,19 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
return out return out
} }
func drawClass(writer io.Writer, targetShape d2target.Shape) { func drawClass(writer io.Writer, diagramHash string, targetShape d2target.Shape) {
el := d2themes.NewThemableElement("rect") el := d2themes.NewThemableElement("rect")
el.X = float64(targetShape.Pos.X) el.X = float64(targetShape.Pos.X)
el.Y = float64(targetShape.Pos.Y) el.Y = float64(targetShape.Pos.Y)
el.Width = float64(targetShape.Width) el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height) el.Height = float64(targetShape.Height)
el.Fill, el.Stroke = d2themes.ShapeTheme(targetShape) el.Fill, el.Stroke = d2themes.ShapeTheme(targetShape)
el.FillPattern = targetShape.FillPattern
el.Style = targetShape.CSSStyle() el.Style = targetShape.CSSStyle()
if targetShape.BorderRadius != 0 {
el.Rx = float64(targetShape.BorderRadius)
el.Ry = float64(targetShape.BorderRadius)
}
fmt.Fprint(writer, el.Render()) fmt.Fprint(writer, el.Render())
box := geo.NewBox( box := geo.NewBox(
@ -100,7 +109,7 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight) headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
fmt.Fprint(writer, fmt.Fprint(writer,
classHeader(targetShape, headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)), classHeader(diagramHash, targetShape, headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
) )
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
@ -113,8 +122,15 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
} }
lineEl := d2themes.NewThemableElement("line") lineEl := d2themes.NewThemableElement("line")
if targetShape.BorderRadius != 0 && len(targetShape.Methods) == 0 {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X+float64(targetShape.BorderRadius), rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width-float64(targetShape.BorderRadius), rowBox.TopLeft.Y
} else {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
}
lineEl.Stroke = targetShape.Fill lineEl.Stroke = targetShape.Fill
lineEl.Style = "stroke-width:1" lineEl.Style = "stroke-width:1"
fmt.Fprint(writer, lineEl.Render()) fmt.Fprint(writer, lineEl.Render())

View file

@ -35,6 +35,7 @@ import (
"oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/d2/lib/svg"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
) )
const ( const (
@ -58,6 +59,15 @@ var baseStylesheet string
//go:embed github-markdown.css //go:embed github-markdown.css
var mdCSS string var mdCSS string
//go:embed dots.txt
var dots string
//go:embed lines.txt
var lines string
//go:embed grain.txt
var grain string
type RenderOpts struct { type RenderOpts struct {
Pad int Pad int
Sketch bool Sketch bool
@ -558,6 +568,9 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
if connection.Label != "" { if connection.Label != "" {
fontClass := "text" fontClass := "text"
if connection.FontFamily == "mono" {
fontClass = "text-mono"
}
if connection.Bold { if connection.Bold {
fontClass += "-bold" fontClass += "-bold"
} else if connection.Italic { } else if connection.Italic {
@ -617,21 +630,22 @@ func renderArrowheadLabel(connection d2target.Connection, text string, position,
return textEl.Render() return textEl.Render()
} }
func renderOval(tl *geo.Point, width, height float64, fill, stroke, style string) string { func renderOval(tl *geo.Point, width, height float64, fill, fillPattern, stroke, style string) string {
el := d2themes.NewThemableElement("ellipse") el := d2themes.NewThemableElement("ellipse")
el.Rx = width / 2 el.Rx = width / 2
el.Ry = height / 2 el.Ry = height / 2
el.Cx = tl.X + el.Rx el.Cx = tl.X + el.Rx
el.Cy = tl.Y + el.Ry el.Cy = tl.Y + el.Ry
el.Fill, el.Stroke = fill, stroke el.Fill, el.Stroke = fill, stroke
el.FillPattern = fillPattern
el.ClassName = "shape" el.ClassName = "shape"
el.Style = style el.Style = style
return el.Render() return el.Render()
} }
func renderDoubleOval(tl *geo.Point, width, height float64, fill, stroke, style string) string { func renderDoubleOval(tl *geo.Point, width, height float64, fill, fillStroke, stroke, style string) string {
var innerTL *geo.Point = tl.AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET)) var innerTL *geo.Point = tl.AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET))
return renderOval(tl, width, height, fill, stroke, style) + renderOval(innerTL, width-10, height-10, fill, stroke, style) return renderOval(tl, width, height, fill, fillStroke, stroke, style) + renderOval(innerTL, width-10, height-10, fill, "", stroke, style)
} }
func defineShadowFilter(writer io.Writer) { func defineShadowFilter(writer io.Writer) {
@ -709,6 +723,7 @@ func render3dRect(targetShape d2target.Shape) string {
mainShape.SetMaskUrl(maskID) mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape) mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
mainShape.Fill = mainShapeFill mainShape.Fill = mainShapeFill
mainShape.FillPattern = targetShape.FillPattern
mainShape.Stroke = color.None mainShape.Stroke = color.None
mainShape.Style = targetShape.CSSStyle() mainShape.Style = targetShape.CSSStyle()
mainShapeRendered := mainShape.Render() mainShapeRendered := mainShape.Render()
@ -829,6 +844,7 @@ func render3dHexagon(targetShape d2target.Shape) string {
mainShape.Points = mainPointsPoly mainShape.Points = mainPointsPoly
mainShape.SetMaskUrl(maskID) mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape) mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
mainShape.FillPattern = targetShape.FillPattern
mainShape.Fill = mainShapeFill mainShape.Fill = mainShapeFill
mainShape.Stroke = color.None mainShape.Stroke = color.None
mainShape.Style = targetShape.CSSStyle() mainShape.Style = targetShape.CSSStyle()
@ -865,7 +881,7 @@ func render3dHexagon(targetShape d2target.Shape) string {
return borderMask + mainShapeRendered + renderedSides + renderedBorder return borderMask + mainShapeRendered + renderedSides + renderedBorder
} }
func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) { func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
closingTag := "</g>" closingTag := "</g>"
if targetShape.Link != "" { if targetShape.Link != "" {
@ -877,6 +893,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if targetShape.Opacity != 1.0 { if targetShape.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity) opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity)
} }
// this clipPath must be defined outside `g` element
if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) {
fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape))
}
fmt.Fprintf(writer, `<g id="%s"%s>`, svg.EscapeText(targetShape.ID), opacityStyle) fmt.Fprintf(writer, `<g id="%s"%s>`, svg.EscapeText(targetShape.ID), opacityStyle)
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)) tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width) width := float64(targetShape.Width)
@ -920,7 +941,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} }
fmt.Fprint(writer, out) fmt.Fprint(writer, out)
} else { } else {
drawClass(writer, targetShape) drawClass(writer, diagramHash, targetShape)
} }
addAppendixItems(writer, targetShape) addAppendixItems(writer, targetShape)
fmt.Fprint(writer, `</g>`) fmt.Fprint(writer, `</g>`)
@ -934,7 +955,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} }
fmt.Fprint(writer, out) fmt.Fprint(writer, out)
} else { } else {
drawTable(writer, targetShape) drawTable(writer, diagramHash, targetShape)
} }
addAppendixItems(writer, targetShape) addAppendixItems(writer, targetShape)
fmt.Fprint(writer, `</g>`) fmt.Fprint(writer, `</g>`)
@ -943,7 +964,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
case d2target.ShapeOval: case d2target.ShapeOval:
if targetShape.DoubleBorder { if targetShape.DoubleBorder {
if targetShape.Multiple { if targetShape.Multiple {
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, stroke, style)) fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style))
} }
if sketchRunner != nil { if sketchRunner != nil {
out, err := d2sketch.DoubleOval(sketchRunner, targetShape) out, err := d2sketch.DoubleOval(sketchRunner, targetShape)
@ -952,11 +973,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} }
fmt.Fprint(writer, out) fmt.Fprint(writer, out)
} else { } else {
fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, stroke, style)) fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
} }
} else { } else {
if targetShape.Multiple { if targetShape.Multiple {
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, stroke, style)) fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style))
} }
if sketchRunner != nil { if sketchRunner != nil {
out, err := d2sketch.Oval(sketchRunner, targetShape) out, err := d2sketch.Oval(sketchRunner, targetShape)
@ -965,7 +986,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} }
fmt.Fprint(writer, out) fmt.Fprint(writer, out)
} else { } else {
fmt.Fprint(writer, renderOval(tl, width, height, fill, stroke, style)) fmt.Fprint(writer, renderOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
} }
} }
@ -1013,6 +1034,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width) el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height) el.Height = float64(targetShape.Height)
el.Fill = fill el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
el.Rx = targetShape.BorderRadius el.Rx = targetShape.BorderRadius
@ -1027,6 +1049,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width) el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height) el.Height = float64(targetShape.Height)
el.Fill = fill el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
el.Rx = targetShape.BorderRadius el.Rx = targetShape.BorderRadius
@ -1058,6 +1081,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width) el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height) el.Height = float64(targetShape.Height)
el.Fill = fill el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
el.Rx = targetShape.BorderRadius el.Rx = targetShape.BorderRadius
@ -1069,7 +1093,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Y = float64(targetShape.Pos.Y + d2target.INNER_BORDER_OFFSET) el.Y = float64(targetShape.Pos.Y + d2target.INNER_BORDER_OFFSET)
el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET) el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET) el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
el.Fill = fill el.Fill = "transparent"
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
el.Rx = targetShape.BorderRadius el.Rx = targetShape.BorderRadius
@ -1103,6 +1127,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else { } else {
el := d2themes.NewThemableElement("path") el := d2themes.NewThemableElement("path")
el.Fill = fill el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
for _, pathData := range s.GetSVGPathData() { for _, pathData := range s.GetSVGPathData() {
@ -1134,6 +1159,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else { } else {
el := d2themes.NewThemableElement("path") el := d2themes.NewThemableElement("path")
el.Fill = fill el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke el.Stroke = stroke
el.Style = style el.Style = style
for _, pathData := range s.GetSVGPathData() { for _, pathData := range s.GetSVGPathData() {
@ -1186,11 +1212,15 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
) )
fontClass := "text" fontClass := "text"
if targetShape.FontFamily == "mono" {
fontClass = "text-mono"
} else {
if targetShape.Bold { if targetShape.Bold {
fontClass += "-bold" fontClass += "-bold"
} else if targetShape.Italic { } else if targetShape.Italic {
fontClass += "-italic" fontClass += "-italic"
} }
}
if targetShape.Underline { if targetShape.Underline {
fontClass += " text-underline" fontClass += " text-underline"
} }
@ -1348,7 +1378,7 @@ func RenderText(text string, x, height float64) string {
return strings.Join(rendered, "") return strings.Join(rendered, "")
} }
func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily) { func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily) {
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`) fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
appendOnTrigger( appendOnTrigger(
@ -1360,13 +1390,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`class="md"`, `class="md"`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text { .%s .text {
font-family: "font-regular"; font-family: "%s-font-regular";
} }
@font-face { @font-face {
font-family: font-regular; font-family: %s-font-regular;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)], d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)],
), ),
) )
@ -1419,13 +1452,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<strong>`, `<strong>`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text-bold { .%s .text-bold {
font-family: "font-bold"; font-family: "%s-font-bold";
} }
@font-face { @font-face {
font-family: font-bold; font-family: %s-font-bold;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)], d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)],
), ),
) )
@ -1439,13 +1475,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<dfn>`, `<dfn>`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text-italic { .%s .text-italic {
font-family: "font-italic"; font-family: "%s-font-italic";
} }
@font-face { @font-face {
font-family: font-italic; font-family: %s-font-italic;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)], d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)],
), ),
) )
@ -1461,13 +1500,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<samp>`, `<samp>`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text-mono { .%s .text-mono {
font-family: "font-mono"; font-family: "%s-font-mono";
} }
@font-face { @font-face {
font-family: font-mono; font-family: %s-font-mono;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR)], d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR)],
), ),
) )
@ -1476,16 +1518,19 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
buf, buf,
source, source,
[]string{ []string{
`class="text-mono-bold"`, `class="text-mono-bold`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text-mono-bold { .%s .text-mono-bold {
font-family: "font-mono-bold"; font-family: "%s-font-mono-bold";
} }
@font-face { @font-face {
font-family: font-mono-bold; font-family: %s-font-mono-bold;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)], d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)],
), ),
) )
@ -1494,52 +1539,19 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
buf, buf,
source, source,
[]string{ []string{
`class="text-mono-italic"`, `class="text-mono-italic`,
}, },
fmt.Sprintf(` fmt.Sprintf(`
.text-mono-italic { .%s .text-mono-italic {
font-family: "font-mono-italic"; font-family: "%s-font-mono-italic";
} }
@font-face { @font-face {
font-family: font-mono-italic; font-family: %s-font-mono-italic;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)],
),
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-mono-bold"`,
},
fmt.Sprintf(`
.text-mono-bold {
font-family: "font-mono-bold";
}
@font-face {
font-family: font-mono-bold;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)],
),
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-mono-italic"`,
},
fmt.Sprintf(`
.text-mono-italic {
font-family: "font-mono-italic";
}
@font-face {
font-family: font-mono-italic;
src: url("%s"); src: url("%s");
}`, }`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)], d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)],
), ),
) )
@ -1644,10 +1656,12 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// Mask URLs are global. So when multiple SVGs attach to a DOM, they share // Mask URLs are global. So when multiple SVGs attach to a DOM, they share
// the same namespace for mask URLs. // the same namespace for mask URLs.
labelMaskID, err := diagram.HashID() diagramHash, err := diagram.HashID()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// CSS names can't start with numbers, so prepend a little something
diagramHash = "d2-" + diagramHash
// SVG has no notion of z-index. The z-index is effectively the order it's drawn. // SVG has no notion of z-index. The z-index is effectively the order it's drawn.
// So draw from the least nested to most nested // So draw from the least nested to most nested
@ -1667,7 +1681,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
markers := map[string]struct{}{} markers := map[string]struct{}{}
for _, obj := range allObjects { for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is { if c, is := obj.(d2target.Connection); is {
labelMask, err := drawConnection(buf, labelMaskID, c, markers, idToShape, sketchRunner) labelMask, err := drawConnection(buf, diagramHash, c, markers, idToShape, sketchRunner)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1675,7 +1689,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
labelMasks = append(labelMasks, labelMask) labelMasks = append(labelMasks, labelMask)
} }
} else if s, is := obj.(d2target.Shape); is { } else if s, is := obj.(d2target.Shape); is {
labelMask, err := drawShape(buf, s, sketchRunner) labelMask, err := drawShape(buf, diagramHash, s, sketchRunner)
if err != nil { if err != nil {
return nil, err return nil, err
} else if labelMask != "" { } else if labelMask != "" {
@ -1690,7 +1704,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
left, top, w, h := dimensions(diagram, pad) left, top, w, h := dimensions(diagram, pad)
fmt.Fprint(buf, strings.Join([]string{ fmt.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`, fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
labelMaskID, left, top, w, h, diagramHash, left, top, w, h,
), ),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`, fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
left, top, w, h, left, top, w, h,
@ -1701,8 +1715,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// generate style elements that will be appended to the SVG tag // generate style elements that will be appended to the SVG tag
upperBuf := &bytes.Buffer{} upperBuf := &bytes.Buffer{}
embedFonts(upperBuf, buf.String(), diagram.FontFamily) // embedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf` embedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily) // embedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
themeStylesheet, err := themeCSS(themeID, darkThemeID) themeStylesheet, err := themeCSS(diagramHash, themeID, darkThemeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1716,8 +1730,14 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
} }
} }
if hasMarkdown { if hasMarkdown {
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, mdCSS) css := mdCSS
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
} }
if sketchRunner != nil { if sketchRunner != nil {
d2sketch.DefineFillPatterns(upperBuf) d2sketch.DefineFillPatterns(upperBuf)
} }
@ -1735,6 +1755,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
backgroundEl.Height = float64(h) backgroundEl.Height = float64(h)
backgroundEl.Fill = diagram.Root.Fill backgroundEl.Fill = diagram.Root.Fill
backgroundEl.Stroke = diagram.Root.Stroke backgroundEl.Stroke = diagram.Root.Stroke
backgroundEl.FillPattern = diagram.Root.FillPattern
backgroundEl.Rx = diagram.Root.BorderRadius backgroundEl.Rx = diagram.Root.BorderRadius
if diagram.Root.StrokeDash != 0 { if diagram.Root.StrokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(diagram.Root.StrokeWidth), diagram.Root.StrokeDash) dashSize, gapSize := svg.GetStrokeDashAttributes(float64(diagram.Root.StrokeWidth), diagram.Root.StrokeDash)
@ -1773,21 +1794,52 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.) h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
} }
bufStr := buf.String()
patternDefs := ""
for _, pattern := range d2graph.FillPatterns {
if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern != "" {
if patternDefs == "" {
fmt.Fprint(upperBuf, `<style type="text/css"><![CDATA[`)
}
switch pattern {
case "dots":
patternDefs += dots
case "lines":
patternDefs += lines
case "grain":
patternDefs += grain
}
fmt.Fprint(upperBuf, fmt.Sprintf(`
.%s-overlay {
fill: url(#%s);
mix-blend-mode: multiply;
}`, pattern, pattern))
}
}
if patternDefs != "" {
fmt.Fprint(upperBuf, `]]></style>`)
fmt.Fprint(upperBuf, "<defs>")
fmt.Fprintf(upperBuf, patternDefs)
fmt.Fprint(upperBuf, "</defs>")
}
var dimensions string var dimensions string
if setDimensions { if setDimensions {
dimensions = fmt.Sprintf(` width="%d" height="%d"`, w, h) dimensions = fmt.Sprintf(` width="%d" height="%d"`, w, h)
} }
fitToScreenWrapper := fmt.Sprintf(`<svg %s preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d"%s>`, fitToScreenWrapper := fmt.Sprintf(`<svg %s d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d"%s>`,
`xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`, `xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`,
version.Version,
w, h, w, h,
dimensions, dimensions,
) )
// TODO minify // TODO minify
docRendered := fmt.Sprintf(`%s%s<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</svg></svg>`, docRendered := fmt.Sprintf(`%s%s<svg id="d2-svg" class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</svg></svg>`,
`<?xml version="1.0" encoding="utf-8"?>`, `<?xml version="1.0" encoding="utf-8"?>`,
fitToScreenWrapper, fitToScreenWrapper,
diagramHash,
w, h, left, top, w, h, w, h, left, top, w, h,
doubleBorderElStr, doubleBorderElStr,
backgroundEl.Render(), backgroundEl.Render(),
@ -1798,14 +1850,14 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
} }
// TODO include only colors that are being used to reduce size // TODO include only colors that are being used to reduce size
func themeCSS(themeID int64, darkThemeID *int64) (stylesheet string, err error) { func themeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(themeID) out, err := singleThemeRulesets(diagramHash, themeID)
if err != nil { if err != nil {
return "", err return "", err
} }
if darkThemeID != nil { if darkThemeID != nil {
darkOut, err := singleThemeRulesets(*darkThemeID) darkOut, err := singleThemeRulesets(diagramHash, *darkThemeID)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -1815,30 +1867,66 @@ func themeCSS(themeID int64, darkThemeID *int64) (stylesheet string, err error)
return out, nil return out, nil
} }
func singleThemeRulesets(themeID int64) (rulesets string, err error) { func singleThemeRulesets(diagramHash string, themeID int64) (rulesets string, err error) {
out := "" out := ""
theme := d2themescatalog.Find(themeID) theme := d2themescatalog.Find(themeID)
// Global theme colors // Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} { for _, property := range []string{"fill", "stroke", "background-color", "color"} {
out += fmt.Sprintf(".%s-N1{%s:%s;}.%s-N2{%s:%s;}.%s-N3{%s:%s;}.%s-N4{%s:%s;}.%s-N5{%s:%s;}.%s-N6{%s:%s;}.%s-N7{%s:%s;}.%s-B1{%s:%s;}.%s-B2{%s:%s;}.%s-B3{%s:%s;}.%s-B4{%s:%s;}.%s-B5{%s:%s;}.%s-B6{%s:%s;}.%s-AA2{%s:%s;}.%s-AA4{%s:%s;}.%s-AA5{%s:%s;}.%s-AB4{%s:%s;}.%s-AB5{%s:%s;}", out += fmt.Sprintf(`
.%s .%s-N1{%s:%s;}
.%s .%s-N2{%s:%s;}
.%s .%s-N3{%s:%s;}
.%s .%s-N4{%s:%s;}
.%s .%s-N5{%s:%s;}
.%s .%s-N6{%s:%s;}
.%s .%s-N7{%s:%s;}
.%s .%s-B1{%s:%s;}
.%s .%s-B2{%s:%s;}
.%s .%s-B3{%s:%s;}
.%s .%s-B4{%s:%s;}
.%s .%s-B5{%s:%s;}
.%s .%s-B6{%s:%s;}
.%s .%s-AA2{%s:%s;}
.%s .%s-AA4{%s:%s;}
.%s .%s-AA5{%s:%s;}
.%s .%s-AB4{%s:%s;}
.%s .%s-AB5{%s:%s;}`,
diagramHash,
property, property, theme.Colors.Neutrals.N1, property, property, theme.Colors.Neutrals.N1,
diagramHash,
property, property, theme.Colors.Neutrals.N2, property, property, theme.Colors.Neutrals.N2,
diagramHash,
property, property, theme.Colors.Neutrals.N3, property, property, theme.Colors.Neutrals.N3,
diagramHash,
property, property, theme.Colors.Neutrals.N4, property, property, theme.Colors.Neutrals.N4,
diagramHash,
property, property, theme.Colors.Neutrals.N5, property, property, theme.Colors.Neutrals.N5,
diagramHash,
property, property, theme.Colors.Neutrals.N6, property, property, theme.Colors.Neutrals.N6,
diagramHash,
property, property, theme.Colors.Neutrals.N7, property, property, theme.Colors.Neutrals.N7,
diagramHash,
property, property, theme.Colors.B1, property, property, theme.Colors.B1,
diagramHash,
property, property, theme.Colors.B2, property, property, theme.Colors.B2,
diagramHash,
property, property, theme.Colors.B3, property, property, theme.Colors.B3,
diagramHash,
property, property, theme.Colors.B4, property, property, theme.Colors.B4,
diagramHash,
property, property, theme.Colors.B5, property, property, theme.Colors.B5,
diagramHash,
property, property, theme.Colors.B6, property, property, theme.Colors.B6,
diagramHash,
property, property, theme.Colors.AA2, property, property, theme.Colors.AA2,
diagramHash,
property, property, theme.Colors.AA4, property, property, theme.Colors.AA4,
diagramHash,
property, property, theme.Colors.AA5, property, property, theme.Colors.AA5,
diagramHash,
property, property, theme.Colors.AB4, property, property, theme.Colors.AB4,
diagramHash,
property, property, theme.Colors.AB5, property, property, theme.Colors.AB5,
) )
} }

View file

@ -430,6 +430,7 @@ func run(t *testing.T, tc testCase) {
Ruler: ruler, Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout, Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn), FontFamily: go2.Pointer(d2fonts.HandDrawn),
ThemeID: 200,
}) })
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 197 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 415 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 300 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View file

@ -0,0 +1,29 @@
<pattern id="dots" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
</pattern>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
<pattern id="lines" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply">
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="2" width="15" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="7" width="15" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="12" width="15" height="1" fill="#0A0F25"/>
</g>
</g>
</pattern>

View file

@ -12,12 +12,44 @@ import (
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
) )
func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { // this func helps define a clipPath for shape class and sql_table to draw border-radius
func clipPathForBorderRadius(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">`, 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.BorderRadius), box.TopLeft.X, box.TopLeft.Y+float64(shape.BorderRadius), box.TopLeft.X, box.TopLeft.Y, box.TopLeft.X+float64(shape.BorderRadius), box.TopLeft.Y)
out += fmt.Sprintf(`L %f %f L %f %f `, box.TopLeft.X+box.Width-float64(shape.BorderRadius), box.TopLeft.Y, topX-float64(shape.BorderRadius), topY)
out += fmt.Sprintf(`S %f %f %f %f `, topX, topY, topX, topY+float64(shape.BorderRadius))
out += fmt.Sprintf(`L %f %f `, topX, topY+box.Height-float64(shape.BorderRadius))
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.BorderRadius), topY+box.Height)
out += fmt.Sprintf(`L %f %f `, box.TopLeft.X+float64(shape.BorderRadius), 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.BorderRadius))
out += fmt.Sprintf(`L %f %f`, box.TopLeft.X, box.TopLeft.Y+float64(shape.BorderRadius))
}
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) string {
rectEl := d2themes.NewThemableElement("rect") rectEl := d2themes.NewThemableElement("rect")
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y
rectEl.Width, rectEl.Height = box.Width, box.Height rectEl.Width, rectEl.Height = box.Width, box.Height
rectEl.Fill = shape.Fill rectEl.Fill = shape.Fill
rectEl.FillPattern = shape.FillPattern
rectEl.ClassName = "class_header" rectEl.ClassName = "class_header"
if shape.BorderRadius != 0 {
rectEl.ClipPath = fmt.Sprintf("%v-%v", diagramHash, shape.ID)
}
str := rectEl.Render() str := rectEl.Render()
if text != "" { if text != "" {
@ -82,15 +114,20 @@ func tableRow(shape d2target.Shape, box *geo.Box, nameText, typeText, constraint
return out return out
} }
func drawTable(writer io.Writer, targetShape d2target.Shape) { func drawTable(writer io.Writer, diagramHash string, targetShape d2target.Shape) {
rectEl := d2themes.NewThemableElement("rect") rectEl := d2themes.NewThemableElement("rect")
rectEl.X = float64(targetShape.Pos.X) rectEl.X = float64(targetShape.Pos.X)
rectEl.Y = float64(targetShape.Pos.Y) rectEl.Y = float64(targetShape.Pos.Y)
rectEl.Width = float64(targetShape.Width) rectEl.Width = float64(targetShape.Width)
rectEl.Height = float64(targetShape.Height) rectEl.Height = float64(targetShape.Height)
rectEl.Fill, rectEl.Stroke = d2themes.ShapeTheme(targetShape) rectEl.Fill, rectEl.Stroke = d2themes.ShapeTheme(targetShape)
rectEl.FillPattern = targetShape.FillPattern
rectEl.ClassName = "shape" rectEl.ClassName = "shape"
rectEl.Style = targetShape.CSSStyle() rectEl.Style = targetShape.CSSStyle()
if targetShape.BorderRadius != 0 {
rectEl.Rx = float64(targetShape.BorderRadius)
rectEl.Ry = float64(targetShape.BorderRadius)
}
fmt.Fprint(writer, rectEl.Render()) fmt.Fprint(writer, rectEl.Render())
box := geo.NewBox( box := geo.NewBox(
@ -102,7 +139,7 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) {
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight) headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
fmt.Fprint(writer, fmt.Fprint(writer,
tableHeader(targetShape, headerBox, targetShape.Label, tableHeader(diagramHash, targetShape, headerBox, targetShape.Label,
float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)), float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
) )
@ -113,15 +150,20 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) {
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
rowBox.TopLeft.Y += headerBox.Height rowBox.TopLeft.Y += headerBox.Height
for _, f := range targetShape.Columns { for idx, f := range targetShape.Columns {
fmt.Fprint(writer, fmt.Fprint(writer,
tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)), tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)),
) )
rowBox.TopLeft.Y += rowHeight rowBox.TopLeft.Y += rowHeight
lineEl := d2themes.NewThemableElement("line") lineEl := d2themes.NewThemableElement("line")
if idx == len(targetShape.Columns)-1 && targetShape.BorderRadius != 0 {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X+float64(targetShape.BorderRadius), rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width-float64(targetShape.BorderRadius), rowBox.TopLeft.Y
} else {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
}
lineEl.Stroke = targetShape.Fill lineEl.Stroke = targetShape.Fill
lineEl.Style = "stroke-width:2" lineEl.Style = "stroke-width:2"
fmt.Fprint(writer, lineEl.Render()) fmt.Fprint(writer, lineEl.Render())

View file

@ -177,6 +177,7 @@ type Shape struct {
BorderRadius float64 `json:"borderRadius"` BorderRadius float64 `json:"borderRadius"`
Fill string `json:"fill"` Fill string `json:"fill"`
FillPattern string `json:"fillPattern,omitempty"`
Stroke string `json:"stroke"` Stroke string `json:"stroke"`
Shadow bool `json:"shadow"` Shadow bool `json:"shadow"`

View file

@ -18,14 +18,10 @@ Run `d2 --help` or `man d2` for more.
# Color coding guide # Color coding guide
To distinguish container nesting, objects get progressively lighter the more nested it is.
<img src="../docs/assets/themes_coding.png" /> <img src="../docs/assets/themes_coding.png" />
# Color coding example # Color coding example
<img src="../docs/assets/themes_coding_example.png" /> <img src="../docs/assets/themes_coding_example.png" />
# Container gradients
To distinguish container nesting, objects get progressively lighter the more nested it is.
<img src="../docs/assets/themes_gradients.png" width="300px" />

View file

@ -8,6 +8,16 @@ type Theme struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Colors ColorPalette `json:"colors"` Colors ColorPalette `json:"colors"`
SpecialRules SpecialRules `json:"specialRules,omitempty"`
}
type SpecialRules struct {
Mono bool `json:"mono"`
NoCornerRadius bool `json:"noCornerRadius"`
OuterContainerDoubleBorder bool `json:"outerContainerDoubleBorder"`
ContainerDots bool `json:"containerDots"`
CapsLock bool `json:"capsLock"`
} }
func (t *Theme) IsDark() bool { func (t *Theme) IsDark() bool {

View file

@ -22,6 +22,8 @@ var LightCatalog = []d2themes.Theme{
EarthTones, EarthTones,
EvergladeGreen, EvergladeGreen,
ButteredToast, ButteredToast,
Terminal,
TerminalGrayscale,
} }
var DarkCatalog = []d2themes.Theme{ var DarkCatalog = []d2themes.Theme{

View file

@ -0,0 +1,42 @@
package d2themescatalog
import "oss.terrastruct.com/d2/d2themes"
var Terminal = d2themes.Theme{
ID: 300,
Name: "Terminal",
Colors: d2themes.ColorPalette{
Neutrals: TerminalNeutral,
B1: "#000410",
B2: "#0000E4",
B3: "#5AA4DC",
B4: "#E7E9EE",
B5: "#F5F6F9",
B6: "#FFFFFF",
AA2: "#008566",
AA4: "#45BBA5",
AA5: "#7ACCBD",
AB4: "#F1C759",
AB5: "#F9E088",
},
SpecialRules: d2themes.SpecialRules{
Mono: true,
NoCornerRadius: true,
OuterContainerDoubleBorder: true,
ContainerDots: true,
CapsLock: true,
},
}
var TerminalNeutral = d2themes.Neutral{
N1: "#000410",
N2: "#0000B8",
N3: "#9499AB",
N4: "#CFD2DD",
N5: "#C3DEF3",
N6: "#EEF1F8",
N7: "#FFFFFF",
}

View file

@ -0,0 +1,42 @@
package d2themescatalog
import "oss.terrastruct.com/d2/d2themes"
var TerminalGrayscale = d2themes.Theme{
ID: 301,
Name: "Terminal Grayscale",
Colors: d2themes.ColorPalette{
Neutrals: TerminalGrayscaleNeutral,
B1: "#000410",
B2: "#000410",
B3: "#FFFFFF",
B4: "#E7E9EE",
B5: "#F5F6F9",
B6: "#FFFFFF",
AA2: "#6D7284",
AA4: "#F5F6F9",
AA5: "#FFFFFF",
AB4: "#F5F6F9",
AB5: "#FFFFFF",
},
SpecialRules: d2themes.SpecialRules{
Mono: true,
NoCornerRadius: true,
OuterContainerDoubleBorder: true,
ContainerDots: true,
CapsLock: true,
},
}
var TerminalGrayscaleNeutral = d2themes.Neutral{
N1: "#000410",
N2: "#000410",
N3: "#9499AB",
N4: "#FFFFFF",
N5: "#FFFFFF",
N6: "#EEF1F8",
N7: "#FFFFFF",
}

View file

@ -46,6 +46,9 @@ type ThemableElement struct {
Attributes string Attributes string
Content string Content string
ClipPath string
FillPattern string
} }
func NewThemableElement(tag string) *ThemableElement { func NewThemableElement(tag string) *ThemableElement {
@ -84,6 +87,8 @@ func NewThemableElement(tag string) *ThemableElement {
"", "",
"", "",
"", "",
"",
"",
} }
} }
@ -201,10 +206,26 @@ func (el *ThemableElement) Render() string {
out += fmt.Sprintf(` %s`, el.Attributes) out += fmt.Sprintf(` %s`, el.Attributes)
} }
if len(el.ClipPath) > 0 {
out += fmt.Sprintf(` clip-path="url(#%s)"`, el.ClipPath)
}
if len(el.Content) > 0 { if len(el.Content) > 0 {
return fmt.Sprintf("%s>%s</%s>", out, el.Content, el.tag) return fmt.Sprintf("%s>%s</%s>", out, el.Content, el.tag)
} }
return out + " />"
out += " />"
if el.FillPattern != "" {
patternEl := el.Copy()
patternEl.Fill = ""
patternEl.Stroke = ""
patternEl.BackgroundColor = ""
patternEl.Color = ""
patternEl.ClassName = fmt.Sprintf("%s-overlay", el.FillPattern)
patternEl.FillPattern = ""
out += patternEl.Render()
}
return out
} }
func calculateAxisRadius(borderRadius float64, sideLength float64) float64 { func calculateAxisRadius(borderRadius float64, sideLength float64) float64 {

View file

@ -0,0 +1,24 @@
package d2themes
import (
"fmt"
)
type PatternOverlay struct {
el *ThemableElement
pattern string
}
func NewPatternOverlay(el *ThemableElement, pattern string) *PatternOverlay {
return &PatternOverlay{
el,
pattern,
}
}
func (o *PatternOverlay) Render() (string, error) {
el := o.el.Copy()
el.Fill = ""
el.ClassName = fmt.Sprintf("%s-overlay", o.pattern)
return el.Render(), nil
}

View file

@ -18,6 +18,7 @@ func NewThemableSketchOverlay(el *ThemableElement, fill string) *ThemableSketchO
} }
} }
// TODO we can just call el.Copy() to prevent that
// WARNING: Do not reuse the element afterwards as this function changes the Class propery // WARNING: Do not reuse the element afterwards as this function changes the Class propery
func (o *ThemableSketchOverlay) Render() (string, error) { func (o *ThemableSketchOverlay) Render() (string, error) {
if color.IsThemeColor(o.fill) { if color.IsThemeColor(o.fill) {

View file

@ -66,8 +66,7 @@ git submodule update --recursive
## Logistics ## Logistics
- Use Go 1.18. Go 1.19's autofmt inexplicably strips spacing from ASCII art in comments. - Use Go 1.20.
We're working on it.
- Please sign your commits - Please sign your commits
([https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730](https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730)). ([https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730](https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730)).
- D2 uses Issues as TODOs. No auto-closing on staleness. - D2 uses Issues as TODOs. No auto-closing on staleness.

View file

@ -158,7 +158,7 @@ You can always install from source:
go install oss.terrastruct.com/d2@latest go install oss.terrastruct.com/d2@latest
``` ```
You need at least Go v1.18 You need at least Go v1.20
### Source Release ### Source Release
@ -175,7 +175,7 @@ fonts and icons. Furthermore, when installing a non versioned commit, installing
will ensure that `d2 --version` works correctly by embedding the commit hash into the `d2` will ensure that `d2 --version` works correctly by embedding the commit hash into the `d2`
binary. binary.
Remember, you need at least Go v1.18 Remember, you need at least Go v1.20
## Windows ## Windows

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -17,6 +17,7 @@ import (
// Remember to add if err != nil checks in production. // Remember to add if err != nil checks in production.
func main() { func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
graph.ApplyTheme(d2themescatalog.NeutralDefault.ID)
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler, nil) _ = graph.SetDimensions(nil, ruler, nil)
_ = d2dagrelayout.Layout(context.Background(), graph, nil) _ = d2dagrelayout.Layout(context.Background(), graph, nil)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Some files were not shown because too many files have changed in this diff Show more