merge with master
34
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
28
ci/release/changelogs/v0.2.5.md
Normal 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:
|
||||||
|
|
||||||
|

|
||||||
|
> [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)
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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,10 +490,8 @@ 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) {
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -525,6 +526,7 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add more, e.g. C, bash
|
// TODO add more, e.g. C, bash
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -609,6 +609,14 @@ y
|
||||||
x -> y
|
x -> y
|
||||||
# foo
|
# foo
|
||||||
y
|
y
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "less_than_edge#955",
|
||||||
|
in: `
|
||||||
|
x <= y
|
||||||
|
`,
|
||||||
|
exp: `x <- = y
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package d2graph
|
|
||||||
|
|
||||||
var systemFonts = []string{
|
|
||||||
"DEFAULT",
|
|
||||||
"SERIOUS",
|
|
||||||
"DIGITAL",
|
|
||||||
"EDUCATIONAL",
|
|
||||||
"NEWSPAPER",
|
|
||||||
"MONO",
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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-
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 228 KiB |
118
d2renderers/d2sketch/testdata/class_and_sqlTable_border_radius/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 325 KiB |
140
d2renderers/d2sketch/testdata/dots-3d/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 228 KiB |
136
d2renderers/d2sketch/testdata/dots-all/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 333 KiB |
136
d2renderers/d2sketch/testdata/dots-multiple/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 336 KiB |
167
d2renderers/d2sketch/testdata/dots-real/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 350 KiB |
149
d2renderers/d2sketch/testdata/dots/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 290 KiB |
103
d2renderers/d2sketch/testdata/elk_corners/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 235 KiB |
106
d2renderers/d2sketch/testdata/opacity/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 107 KiB |
150
d2renderers/d2sketch/testdata/terminal/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 355 KiB |
106
d2renderers/d2sketch/testdata/twitter/sketch.exp.svg
vendored
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 420 KiB |
|
|
@ -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
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 978 KiB |
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 578 KiB After Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 321 KiB |
29
d2renderers/d2svg/dots.txt
Normal 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>
|
||||||
14
d2renderers/d2svg/grain.txt
Normal file
13
d2renderers/d2svg/lines.txt
Normal 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>
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ var LightCatalog = []d2themes.Theme{
|
||||||
EarthTones,
|
EarthTones,
|
||||||
EvergladeGreen,
|
EvergladeGreen,
|
||||||
ButteredToast,
|
ButteredToast,
|
||||||
|
Terminal,
|
||||||
|
TerminalGrayscale,
|
||||||
}
|
}
|
||||||
|
|
||||||
var DarkCatalog = []d2themes.Theme{
|
var DarkCatalog = []d2themes.Theme{
|
||||||
|
|
|
||||||
42
d2themes/d2themescatalog/terminal.go
Normal 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",
|
||||||
|
}
|
||||||
42
d2themes/d2themescatalog/terminal_grayscale.go
Normal 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",
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
24
d2themes/pattern_overlay.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -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)
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 330 KiB |