Skip to content

Commit

Permalink
Merge pull request #492 from alixander/sketch
Browse files Browse the repository at this point in the history
Sketch (hand-drawn mode)
  • Loading branch information
alixander authored Dec 22, 2022
2 parents 10c212f + d41b98f commit 716f9d4
Show file tree
Hide file tree
Showing 219 changed files with 3,766 additions and 64 deletions.
2 changes: 2 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#### Features 🚀

- `sketch` flag renders the diagram to look like it was sketched by hand. [#492](https://github.com/terrastruct/d2/pull/492)

#### Improvements 🧹

- Improved label placements for shapes with images to avoid overlapping container labels. [#474](https://github.com/terrastruct/d2/pull/474)
Expand Down
3 changes: 3 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ Port listening address when used with
Set the diagram theme to the passed integer. For a list of available options, see
.Lk https://oss.terrastruct.com/d2
.Ns .
.It Fl s , -sketch Ar false
Renders the diagram to look like it was sketched by hand
.Ns .
.It Fl -pad Ar 100
Pixels padded around the rendered diagram
.Ns .
Expand Down
4 changes: 2 additions & 2 deletions d2chaos/d2chaos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ func test(t *testing.T, textPath, text string) {
ruler, err := textmeasure.NewRuler()
assert.Nil(t, err)

err = g.SetDimensions(nil, ruler)
err = g.SetDimensions(nil, ruler, nil)
assert.Nil(t, err)

err = d2dagrelayout.Layout(ctx, g)
if err != nil {
t.Fatal(err)
}

_, err = d2exporter.Export(ctx, g, 0)
_, err = d2exporter.Export(ctx, g, 0, nil)
if err != nil {
t.Fatal(err)
}
Expand Down
8 changes: 7 additions & 1 deletion d2exporter/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import (
"strconv"

"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
)

func Export(ctx context.Context, g *d2graph.Graph, themeID int64) (*d2target.Diagram, error) {
func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
theme := d2themescatalog.Find(themeID)

diagram := d2target.NewDiagram()
if fontFamily == nil {
defaultFont := d2fonts.SourceSansPro
fontFamily = &defaultFont
}
diagram.FontFamily = fontFamily

diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range g.Objects {
Expand Down
4 changes: 2 additions & 2 deletions d2exporter/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ func run(t *testing.T, tc testCase) {
ruler, err := textmeasure.NewRuler()
assert.JSON(t, nil, err)

err = g.SetDimensions(nil, ruler)
err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err)

err = d2dagrelayout.Layout(ctx, g)
if err != nil {
t.Fatal(err)
}

got, err := d2exporter.Export(ctx, g, tc.themeID)
got, err := d2exporter.Export(ctx, g, tc.themeID, nil)
if err != nil {
t.Fatal(err)
}
Expand Down
24 changes: 14 additions & 10 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
return nil, fmt.Errorf("text not pre-measured and no ruler provided")
}

func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText) *d2target.TextDimensions {
func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions {
if dims := findMeasured(mtexts, t); dims != nil {
return dims
}
Expand All @@ -861,7 +861,11 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
} else if t.IsItalic {
style = d2fonts.FONT_STYLE_ITALIC
}
w, h = ruler.Measure(d2fonts.SourceSansPro.Font(t.FontSize, style), t.Text)
if fontFamily == nil {
defaultFont := d2fonts.SourceSansPro
fontFamily = &defaultFont
}
w, h = ruler.Measure(fontFamily.Font(t.FontSize, style), t.Text)
}
return d2target.NewTextDimensions(w, h)
}
Expand All @@ -870,13 +874,13 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
}

func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText {
if getTextDimensions(texts, nil, t) == nil {
if getTextDimensions(texts, nil, t, nil) == nil {
return append(texts, t)
}
return texts
}

func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler) error {
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) error {
for _, obj := range g.Objects {
obj.Box = &geo.Box{}
// TODO fix edge cases for unnamed class etc
Expand Down Expand Up @@ -905,7 +909,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
innerLabelPadding = 0
} else {
dims = getTextDimensions(mtexts, ruler, obj.Text())
dims = getTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
}
if dims == nil {
if obj.Attributes.Shape.Value == d2target.ShapeImage {
Expand Down Expand Up @@ -959,7 +963,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
maxWidth := dims.Width

for _, f := range obj.Class.Fields {
fdims := getTextDimensions(mtexts, ruler, f.Text())
fdims := getTextDimensions(mtexts, ruler, f.Text(), fontFamily)
if fdims == nil {
return fmt.Errorf("dimensions for class field %#v not found", f.Text())
}
Expand All @@ -969,7 +973,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
for _, m := range obj.Class.Methods {
mdims := getTextDimensions(mtexts, ruler, m.Text())
mdims := getTextDimensions(mtexts, ruler, m.Text(), fontFamily)
if mdims == nil {
return fmt.Errorf("dimensions for class method %#v not found", m.Text())
}
Expand All @@ -988,7 +992,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
if anyRowText != nil {
// 10px of padding top and bottom so text doesn't look squished
rowHeight := getTextDimensions(mtexts, ruler, anyRowText).Height + 20
rowHeight := getTextDimensions(mtexts, ruler, anyRowText, fontFamily).Height + 20
obj.Height = float64(rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2))
}
// Leave room for padding
Expand Down Expand Up @@ -1043,7 +1047,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
for _, label := range endpointLabels {
t := edge.Text()
t.Text = label
dims := getTextDimensions(mtexts, ruler, t)
dims := getTextDimensions(mtexts, ruler, t, fontFamily)
edge.MinWidth += dims.Width
// Some padding as it's not totally near the end
edge.MinHeight += dims.Height + 5
Expand All @@ -1053,7 +1057,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}

dims := getTextDimensions(mtexts, ruler, edge.Text())
dims := getTextDimensions(mtexts, ruler, edge.Text(), fontFamily)
if dims == nil {
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
}
Expand Down
13 changes: 10 additions & 3 deletions d2lib/d2.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/textmeasure"
)
Expand All @@ -20,7 +21,13 @@ type CompileOptions struct {
Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error

ThemeID int64
// FontFamily controls the font family used for all texts that are not the following:
// - code
// - latex
// - pre-measured (web setting)
// TODO maybe some will want to configure code font too, but that's much lower priority
FontFamily *d2fonts.FontFamily
ThemeID int64
}

func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) {
Expand All @@ -36,7 +43,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}

if len(g.Objects) > 0 {
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler)
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil {
return nil, nil, err
}
Expand All @@ -48,7 +55,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
}

diagram, err := d2exporter.Export(ctx, g, opts.ThemeID)
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily)
return diagram, g, err
}

Expand Down
48 changes: 45 additions & 3 deletions d2renderers/d2fonts/d2fonts.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// d2fonts holds fonts for renderings

// TODO write a script to do this as part of CI
// Currently using an online converter: https://dopiaza.org/tools/datauri/index.php
package d2fonts

import (
"embed"
"strings"
)

type FontFamily int
type FontFamily string
type FontStyle string

type Font struct {
Expand Down Expand Up @@ -38,8 +39,9 @@ const (
FONT_STYLE_BOLD FontStyle = "bold"
FONT_STYLE_ITALIC FontStyle = "italic"

SourceSansPro FontFamily = iota
SourceCodePro FontFamily = iota
SourceSansPro FontFamily = "SourceSansPro"
SourceCodePro FontFamily = "SourceCodePro"
HandDrawn FontFamily = "HandDrawn"
)

var FontSizes = []int{
Expand All @@ -61,6 +63,7 @@ var FontStyles = []FontStyle{
var FontFamilies = []FontFamily{
SourceSansPro,
SourceCodePro,
HandDrawn,
}

//go:embed encoded/SourceSansPro-Regular.txt
Expand All @@ -75,6 +78,12 @@ var sourceSansProItalicBase64 string
//go:embed encoded/SourceCodePro-Regular.txt
var sourceCodeProRegularBase64 string

//go:embed encoded/ArchitectsDaughter-Regular.txt
var architectsDaughterRegularBase64 string

//go:embed encoded/FuzzyBubbles-Bold.txt
var fuzzyBubblesBoldBase64 string

//go:embed ttf/*
var fontFacesFS embed.FS

Expand All @@ -99,6 +108,19 @@ func init() {
Family: SourceCodePro,
Style: FONT_STYLE_REGULAR,
}: sourceCodeProRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_REGULAR,
}: architectsDaughterRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_ITALIC,
// This font has no italic, so just reuse regular
}: architectsDaughterRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}: fuzzyBubblesBoldBase64,
}

for k, v := range FontEncodings {
Expand Down Expand Up @@ -138,4 +160,24 @@ func init() {
Family: SourceSansPro,
Style: FONT_STYLE_ITALIC,
}] = b
b, err = fontFacesFS.ReadFile("ttf/ArchitectsDaughter-Regular.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_REGULAR,
}] = b
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_ITALIC,
}] = b
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}] = b
}
1 change: 1 addition & 0 deletions d2renderers/d2fonts/encoded/ArchitectsDaughter-Regular.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions d2renderers/d2fonts/encoded/FuzzyBubbles-Bold.txt

Large diffs are not rendered by default.

Binary file not shown.
Binary file added d2renderers/d2fonts/ttf/FuzzyBubbles-Bold.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions d2renderers/d2sketch/fillpattern.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 716f9d4

Please sign in to comment.