animate-interval
|
|
@ -1,5 +1,6 @@
|
||||||
#### Features 🚀
|
#### Features 🚀
|
||||||
|
|
||||||
|
- `--animate-interval` can be passed as a flag to animate multi-board diagrams. See [docs](https://d2lang.com/todo). [#1088](https://github.com/terrastruct/d2/pull/1088)
|
||||||
- `paper` is available as a `fill-pattern` option [#1070](https://github.com/terrastruct/d2/pull/1070)
|
- `paper` is available as a `fill-pattern` option [#1070](https://github.com/terrastruct/d2/pull/1070)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### Improvements 🧹
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,9 @@ Center the SVG in the containing viewbox, such as your browser screen
|
||||||
.It Fl -pad Ar 100
|
.It Fl -pad Ar 100
|
||||||
Pixels padded around the rendered diagram
|
Pixels padded around the rendered diagram
|
||||||
.Ns .
|
.Ns .
|
||||||
|
.It Fl -animate-interval Ar 0
|
||||||
|
If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports
|
||||||
|
.Ns .
|
||||||
.It Fl -browser Ar true
|
.It Fl -browser Ar true
|
||||||
Browser executable that watch opens. Setting to 0 opens no browser
|
Browser executable that watch opens. Setting to 0 opens no browser
|
||||||
.Ns .
|
.Ns .
|
||||||
|
|
|
||||||
170
d2cli/main.go
|
|
@ -21,6 +21,7 @@ import (
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2lib"
|
"oss.terrastruct.com/d2/d2lib"
|
||||||
"oss.terrastruct.com/d2/d2plugin"
|
"oss.terrastruct.com/d2/d2plugin"
|
||||||
|
"oss.terrastruct.com/d2/d2renderers/d2animate"
|
||||||
"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/d2renderers/d2svg/appendix"
|
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
|
||||||
|
|
@ -68,7 +69,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "The theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
|
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +77,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
animateIntervalFlag, err := ms.Opts.Int64("D2_ANIMATE_INTERVAL", "animate-interval", "", 0, "if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports.")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
|
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -171,6 +176,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
}
|
}
|
||||||
if outputPath != "-" {
|
if outputPath != "-" {
|
||||||
outputPath = ms.AbsPath(outputPath)
|
outputPath = ms.AbsPath(outputPath)
|
||||||
|
if *animateIntervalFlag > 0 {
|
||||||
|
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
|
||||||
|
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
|
||||||
|
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match := d2themescatalog.Find(*themeFlag)
|
match := d2themescatalog.Find(*themeFlag)
|
||||||
|
|
@ -231,24 +242,29 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderOpts := d2svg.RenderOpts{
|
||||||
|
Pad: int(*padFlag),
|
||||||
|
Sketch: *sketchFlag,
|
||||||
|
Center: *centerFlag,
|
||||||
|
ThemeID: *themeFlag,
|
||||||
|
DarkThemeID: darkThemeFlag,
|
||||||
|
}
|
||||||
|
|
||||||
if *watchFlag {
|
if *watchFlag {
|
||||||
if inputPath == "-" {
|
if inputPath == "-" {
|
||||||
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
|
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
|
||||||
}
|
}
|
||||||
w, err := newWatcher(ctx, ms, watcherOpts{
|
w, err := newWatcher(ctx, ms, watcherOpts{
|
||||||
layoutPlugin: plugin,
|
layoutPlugin: plugin,
|
||||||
sketch: *sketchFlag,
|
renderOpts: renderOpts,
|
||||||
center: *centerFlag,
|
animateInterval: *animateIntervalFlag,
|
||||||
themeID: *themeFlag,
|
host: *hostFlag,
|
||||||
darkThemeID: darkThemeFlag,
|
port: *portFlag,
|
||||||
pad: *padFlag,
|
inputPath: inputPath,
|
||||||
host: *hostFlag,
|
outputPath: outputPath,
|
||||||
port: *portFlag,
|
bundle: *bundleFlag,
|
||||||
inputPath: inputPath,
|
forceAppendix: *forceAppendixFlag,
|
||||||
outputPath: outputPath,
|
pw: pw,
|
||||||
bundle: *bundleFlag,
|
|
||||||
forceAppendix: *forceAppendixFlag,
|
|
||||||
pw: pw,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -259,7 +275,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
|
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, written, err := compile(ctx, ms, plugin, *sketchFlag, *centerFlag, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
|
_, written, err := compile(ctx, ms, plugin, renderOpts, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if written {
|
if written {
|
||||||
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
|
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
|
||||||
|
|
@ -269,7 +285,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, renderOpts d2svg.RenderOpts, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
input, err := ms.ReadPath(inputPath)
|
input, err := ms.ReadPath(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -285,9 +301,9 @@ 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,
|
ThemeID: renderOpts.ThemeID,
|
||||||
}
|
}
|
||||||
if sketch {
|
if renderOpts.Sketch {
|
||||||
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
|
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,6 +318,14 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
if animateInterval > 0 {
|
||||||
|
masterID, err := diagram.HashID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
renderOpts.MasterID = masterID
|
||||||
|
}
|
||||||
|
|
||||||
pluginInfo, err := plugin.Info(ctx)
|
pluginInfo, err := plugin.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
|
@ -312,27 +336,42 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var svg []byte
|
|
||||||
if filepath.Ext(outputPath) == ".pdf" {
|
if filepath.Ext(outputPath) == ".pdf" {
|
||||||
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
||||||
svg, err = renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
||||||
} else {
|
if err != nil {
|
||||||
compileDur := time.Since(start)
|
return pdf, false, err
|
||||||
svg, err = render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
}
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return svg, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if filepath.Ext(outputPath) == ".pdf" {
|
|
||||||
dur := time.Since(start)
|
dur := time.Since(start)
|
||||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
|
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
|
||||||
|
return pdf, true, nil
|
||||||
|
} else {
|
||||||
|
compileDur := time.Since(start)
|
||||||
|
boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
out := boards[0]
|
||||||
|
if animateInterval > 0 {
|
||||||
|
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
err = ms.WritePath(outputPath, out)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
|
||||||
|
}
|
||||||
|
return out, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return svg, true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, sketch, center bool, pad int64, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
|
||||||
if diagram.Name != "" {
|
if diagram.Name != "" {
|
||||||
ext := filepath.Ext(outputPath)
|
ext := filepath.Ext(outputPath)
|
||||||
outputPath = strings.TrimSuffix(outputPath, ext)
|
outputPath = strings.TrimSuffix(outputPath, ext)
|
||||||
|
|
@ -343,6 +382,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
boardOutputPath := outputPath
|
boardOutputPath := outputPath
|
||||||
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
|
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
|
||||||
if outputPath == "-" {
|
if outputPath == "-" {
|
||||||
|
// TODO it can if composed into one
|
||||||
return nil, fmt.Errorf("multiboard output cannot be written to stdout")
|
return nil, fmt.Errorf("multiboard output cannot be written to stdout")
|
||||||
}
|
}
|
||||||
// Boards with subboards must be self-contained folders.
|
// Boards with subboards must be self-contained folders.
|
||||||
|
|
@ -375,47 +415,55 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
stepsOutputPath += ext
|
stepsOutputPath += ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var boards [][]byte
|
||||||
for _, dl := range diagram.Layers {
|
for _, dl := range diagram.Layers {
|
||||||
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
for _, dl := range diagram.Scenarios {
|
||||||
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
for _, dl := range diagram.Steps {
|
||||||
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
|
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !diagram.IsFolderOnly {
|
if !diagram.IsFolderOnly {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
svg, err := _render(ctx, ms, plugin, sketch, center, pad, themeID, darkThemeID, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
|
out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
return boards, err
|
||||||
}
|
}
|
||||||
dur := compileDur + time.Since(start)
|
dur := compileDur + time.Since(start)
|
||||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
|
if opts.MasterID == "" {
|
||||||
return svg, nil
|
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
|
||||||
|
}
|
||||||
|
boards = append([][]byte{out}, boards...)
|
||||||
|
return boards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad int64, themeID int64, darkThemeID *int64, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||||
toPNG := filepath.Ext(outputPath) == ".png"
|
toPNG := filepath.Ext(outputPath) == ".png"
|
||||||
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: int(pad),
|
Pad: opts.Pad,
|
||||||
Sketch: sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: center,
|
Center: opts.Center,
|
||||||
ThemeID: themeID,
|
ThemeID: opts.ThemeID,
|
||||||
DarkThemeID: darkThemeID,
|
DarkThemeID: opts.DarkThemeID,
|
||||||
|
MasterID: opts.MasterID,
|
||||||
SetDimensions: toPNG,
|
SetDimensions: toPNG,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -461,13 +509,15 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
if opts.MasterID == "" {
|
||||||
if err != nil {
|
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||||
return svg, err
|
if err != nil {
|
||||||
}
|
return svg, err
|
||||||
err = ms.WritePath(outputPath, out)
|
}
|
||||||
if err != nil {
|
err = ms.WritePath(outputPath, out)
|
||||||
return svg, err
|
if err != nil {
|
||||||
|
return svg, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if bundleErr != nil {
|
if bundleErr != nil {
|
||||||
return svg, bundleErr
|
return svg, bundleErr
|
||||||
|
|
@ -475,7 +525,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
||||||
return svg, nil
|
return svg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
|
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
|
||||||
var isRoot bool
|
var isRoot bool
|
||||||
if pdf == nil {
|
if pdf == nil {
|
||||||
pdf = pdflib.Init()
|
pdf = pdflib.Init()
|
||||||
|
|
@ -501,9 +551,9 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
|
||||||
diagram.Root.Fill = "transparent"
|
diagram.Root.Fill = "transparent"
|
||||||
|
|
||||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||||
Pad: int(pad),
|
Pad: opts.Pad,
|
||||||
Sketch: sketch,
|
Sketch: opts.Sketch,
|
||||||
Center: center,
|
Center: opts.Center,
|
||||||
SetDimensions: true,
|
SetDimensions: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -537,26 +587,26 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY, pageMap)
|
err = pdf.AddPDFPage(pngImg, currBoardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dl := range diagram.Layers {
|
for _, dl := range diagram.Layers {
|
||||||
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
for _, dl := range diagram.Scenarios {
|
||||||
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
for _, dl := range diagram.Steps {
|
||||||
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2plugin"
|
"oss.terrastruct.com/d2/d2plugin"
|
||||||
|
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||||
"oss.terrastruct.com/d2/lib/png"
|
"oss.terrastruct.com/d2/lib/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -39,20 +40,17 @@ var devMode = false
|
||||||
var staticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
type watcherOpts struct {
|
type watcherOpts struct {
|
||||||
layoutPlugin d2plugin.Plugin
|
layoutPlugin d2plugin.Plugin
|
||||||
themeID int64
|
renderOpts d2svg.RenderOpts
|
||||||
darkThemeID *int64
|
animateInterval int64
|
||||||
pad int64
|
host string
|
||||||
sketch bool
|
port string
|
||||||
center bool
|
inputPath string
|
||||||
host string
|
outputPath string
|
||||||
port string
|
pwd string
|
||||||
inputPath string
|
bundle bool
|
||||||
outputPath string
|
forceAppendix bool
|
||||||
pwd string
|
pw png.Playwright
|
||||||
bundle bool
|
|
||||||
forceAppendix bool
|
|
||||||
pw png.Playwright
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type watcher struct {
|
type watcher struct {
|
||||||
|
|
@ -360,7 +358,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
|
||||||
w.pw = newPW
|
w.pw = newPW
|
||||||
}
|
}
|
||||||
|
|
||||||
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.center, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
|
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.renderOpts, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
|
||||||
errs := ""
|
errs := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(svg) > 0 {
|
if len(svg) > 0 {
|
||||||
|
|
|
||||||
116
d2renderers/d2animate/d2animate.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package d2animate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/d2/d2renderers/d2sketch"
|
||||||
|
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||||
|
"oss.terrastruct.com/d2/d2target"
|
||||||
|
"oss.terrastruct.com/d2/lib/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var transitionDurationMS = 1
|
||||||
|
|
||||||
|
func makeKeyframe(delayMS, durationMS, totalMS, identifier int) string {
|
||||||
|
percentageBefore := (math.Max(0, float64(delayMS-transitionDurationMS)) / float64(totalMS)) * 100.
|
||||||
|
percentageStart := (float64(delayMS) / float64(totalMS)) * 100.
|
||||||
|
percentageEnd := (float64(delayMS+durationMS-transitionDurationMS) / float64(totalMS)) * 100.
|
||||||
|
if int(math.Ceil(percentageEnd)) == 100 {
|
||||||
|
return fmt.Sprintf(`@keyframes d2Transition-%d {
|
||||||
|
0%%, %f%% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
%f%%, %f%% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}`, identifier, percentageBefore, percentageStart, math.Ceil(percentageEnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageAfter := (float64(delayMS+durationMS) / float64(totalMS)) * 100.
|
||||||
|
return fmt.Sprintf(`@keyframes d2Transition-%d {
|
||||||
|
0%%, %f%% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
%f%%, %f%% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
%f%%, 100%% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}`, identifier, percentageBefore, percentageStart, percentageEnd, percentageAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderOpts, intervalMS int) ([]byte, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
// TODO account for stroke width of root border
|
||||||
|
|
||||||
|
tl, br := rootDiagram.NestedBoundingBox()
|
||||||
|
left := tl.X - renderOpts.Pad
|
||||||
|
top := tl.Y - renderOpts.Pad
|
||||||
|
width := br.X - tl.X + renderOpts.Pad*2
|
||||||
|
height := br.Y - tl.Y + renderOpts.Pad*2
|
||||||
|
|
||||||
|
fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d">`,
|
||||||
|
version.Version,
|
||||||
|
width, height,
|
||||||
|
)
|
||||||
|
fmt.Fprint(buf, fitToScreenWrapperOpening)
|
||||||
|
|
||||||
|
innerOpening := fmt.Sprintf(`<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
|
||||||
|
width, height, left, top, width, height)
|
||||||
|
fmt.Fprint(buf, innerOpening)
|
||||||
|
|
||||||
|
svgsStr := ""
|
||||||
|
for _, svg := range svgs {
|
||||||
|
svgsStr += string(svg) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
diagramHash, err := rootDiagram.HashID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d2svg.EmbedFonts(buf, diagramHash, svgsStr, rootDiagram.FontFamily)
|
||||||
|
|
||||||
|
themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buf, `<style type="text/css"><![CDATA[%s%s]]></style>`, d2svg.BaseStylesheet, themeStylesheet)
|
||||||
|
|
||||||
|
if rootDiagram.HasShape(func(s d2target.Shape) bool {
|
||||||
|
return s.Label != "" && s.Type == d2target.ShapeText
|
||||||
|
}) {
|
||||||
|
css := d2svg.MarkdownCSS
|
||||||
|
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(buf, `<style type="text/css">%s</style>`, css)
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderOpts.Sketch {
|
||||||
|
d2sketch.DefineFillPatterns(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
|
||||||
|
for i := range svgs {
|
||||||
|
fmt.Fprint(buf, makeKeyframe(i*intervalMS, intervalMS, len(svgs)*intervalMS, i))
|
||||||
|
}
|
||||||
|
fmt.Fprint(buf, `]]></style>`)
|
||||||
|
|
||||||
|
for i, svg := range svgs {
|
||||||
|
str := string(svg)
|
||||||
|
str = strings.Replace(str, "<g", fmt.Sprintf(`<g style="animation: d2Transition-%d %dms infinite"`, i, len(svgs)*intervalMS), 1)
|
||||||
|
buf.Write([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(buf, "</svg>")
|
||||||
|
fmt.Fprint(buf, "</svg>")
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
@ -54,10 +54,10 @@ var TooltipIcon string
|
||||||
var LinkIcon string
|
var LinkIcon string
|
||||||
|
|
||||||
//go:embed style.css
|
//go:embed style.css
|
||||||
var baseStylesheet string
|
var BaseStylesheet string
|
||||||
|
|
||||||
//go:embed github-markdown.css
|
//go:embed github-markdown.css
|
||||||
var mdCSS string
|
var MarkdownCSS string
|
||||||
|
|
||||||
//go:embed dots.txt
|
//go:embed dots.txt
|
||||||
var dots string
|
var dots string
|
||||||
|
|
@ -79,6 +79,10 @@ type RenderOpts struct {
|
||||||
DarkThemeID *int64
|
DarkThemeID *int64
|
||||||
// disables the fit to screen behavior and ensures the exported svg has the exact dimensions
|
// disables the fit to screen behavior and ensures the exported svg has the exact dimensions
|
||||||
SetDimensions bool
|
SetDimensions bool
|
||||||
|
|
||||||
|
// MasterID is passed when the diagram should use something other than its own hash for unique targeting
|
||||||
|
// Currently, that's when multi-boards are collapsed
|
||||||
|
MasterID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
|
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
|
||||||
|
|
@ -1382,7 +1386,7 @@ func RenderText(text string, x, height float64) string {
|
||||||
return strings.Join(rendered, "")
|
return strings.Join(rendered, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func embedFonts(buf *bytes.Buffer, diagramHash, 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(
|
||||||
|
|
@ -1658,14 +1662,16 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mask URLs are global. So when multiple SVGs attach to a DOM, they share
|
// Apply hash on IDs for targeting, to be specific for this diagram
|
||||||
// the same namespace for mask URLs.
|
|
||||||
diagramHash, 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
|
// Some targeting is still per-board, like masks for connections
|
||||||
diagramHash = "d2-" + diagramHash
|
isolatedDiagramHash := diagramHash
|
||||||
|
if opts != nil && opts.MasterID != "" {
|
||||||
|
diagramHash = opts.MasterID
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -1685,7 +1691,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, diagramHash, c, markers, idToShape, sketchRunner)
|
labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, sketchRunner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1708,7 +1714,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">`,
|
||||||
diagramHash, left, top, w, h,
|
isolatedDiagramHash, 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,
|
||||||
|
|
@ -1719,31 +1725,33 @@ 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, diagramHash, buf.String(), diagram.FontFamily) // embedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
|
if opts.MasterID == "" {
|
||||||
themeStylesheet, err := themeCSS(diagramHash, themeID, darkThemeID)
|
EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
|
||||||
if err != nil {
|
themeStylesheet, err := ThemeCSS(diagramHash, themeID, darkThemeID)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, baseStylesheet, themeStylesheet)
|
|
||||||
|
|
||||||
hasMarkdown := false
|
|
||||||
for _, s := range diagram.Shapes {
|
|
||||||
if s.Label != "" && s.Type == d2target.ShapeText {
|
|
||||||
hasMarkdown = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, BaseStylesheet, themeStylesheet)
|
||||||
if hasMarkdown {
|
|
||||||
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 {
|
hasMarkdown := false
|
||||||
d2sketch.DefineFillPatterns(upperBuf)
|
for _, s := range diagram.Shapes {
|
||||||
|
if s.Label != "" && s.Type == d2target.ShapeText {
|
||||||
|
hasMarkdown = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasMarkdown {
|
||||||
|
css := MarkdownCSS
|
||||||
|
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 {
|
||||||
|
d2sketch.DefineFillPatterns(upperBuf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shift is for background el to envelop the diagram
|
// This shift is for background el to envelop the diagram
|
||||||
|
|
@ -1838,30 +1846,45 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
if opts.Center {
|
if opts.Center {
|
||||||
alignment = "xMidYMid"
|
alignment = "xMidYMid"
|
||||||
}
|
}
|
||||||
fitToScreenWrapper := fmt.Sprintf(`<svg %s d2Version="%s" preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>`,
|
fitToScreenWrapperOpening := ""
|
||||||
`xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`,
|
xmlTag := ""
|
||||||
version.Version,
|
fitToScreenWrapperClosing := ""
|
||||||
alignment,
|
idAttr := ""
|
||||||
w, h,
|
tag := "g"
|
||||||
dimensions,
|
// Many things change when this is rendering for animation
|
||||||
)
|
if opts.MasterID == "" {
|
||||||
|
fitToScreenWrapperOpening = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>`,
|
||||||
|
version.Version,
|
||||||
|
alignment,
|
||||||
|
w, h,
|
||||||
|
dimensions,
|
||||||
|
)
|
||||||
|
xmlTag = `<?xml version="1.0" encoding="utf-8"?>`
|
||||||
|
fitToScreenWrapperClosing = "</svg>"
|
||||||
|
idAttr = `id="d2-svg"`
|
||||||
|
tag = "svg"
|
||||||
|
}
|
||||||
|
|
||||||
// TODO minify
|
// TODO minify
|
||||||
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>`,
|
docRendered := fmt.Sprintf(`%s%s<%s %s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s`,
|
||||||
`<?xml version="1.0" encoding="utf-8"?>`,
|
xmlTag,
|
||||||
fitToScreenWrapper,
|
fitToScreenWrapperOpening,
|
||||||
|
tag,
|
||||||
|
idAttr,
|
||||||
diagramHash,
|
diagramHash,
|
||||||
w, h, left, top, w, h,
|
w, h, left, top, w, h,
|
||||||
doubleBorderElStr,
|
doubleBorderElStr,
|
||||||
backgroundEl.Render(),
|
backgroundEl.Render(),
|
||||||
upperBuf.String(),
|
upperBuf.String(),
|
||||||
buf.String(),
|
buf.String(),
|
||||||
|
tag,
|
||||||
|
fitToScreenWrapperClosing,
|
||||||
)
|
)
|
||||||
return []byte(docRendered), nil
|
return []byte(docRendered), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO include only colors that are being used to reduce size
|
// TODO include only colors that are being used to reduce size
|
||||||
func themeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
|
func ThemeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
|
||||||
out, err := singleThemeRulesets(diagramHash, themeID)
|
out, err := singleThemeRulesets(diagramHash, themeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -51,18 +51,101 @@ type Diagram struct {
|
||||||
Steps []*Diagram `json:"steps,omitempty"`
|
Steps []*Diagram `json:"steps,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (diagram Diagram) HashID() (string, error) {
|
func (diagram Diagram) Bytes() ([]byte, error) {
|
||||||
b1, err := json.Marshal(diagram.Shapes)
|
b1, err := json.Marshal(diagram.Shapes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
b2, err := json.Marshal(diagram.Connections)
|
b2, err := json.Marshal(diagram.Connections)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
base := append(b1, b2...)
|
||||||
|
|
||||||
|
for _, d := range diagram.Layers {
|
||||||
|
slices, err := d.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
base = append(base, slices...)
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Scenarios {
|
||||||
|
slices, err := d.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
base = append(base, slices...)
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Steps {
|
||||||
|
slices, err := d.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
base = append(base, slices...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (diagram Diagram) HasShape(condition func(Shape) bool) bool {
|
||||||
|
for _, d := range diagram.Layers {
|
||||||
|
if d.HasShape(condition) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Scenarios {
|
||||||
|
if d.HasShape(condition) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Steps {
|
||||||
|
if d.HasShape(condition) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range diagram.Shapes {
|
||||||
|
if condition(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (diagram Diagram) HashID() (string, error) {
|
||||||
|
bytes, err := diagram.Bytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
h := fnv.New32a()
|
h := fnv.New32a()
|
||||||
h.Write(append(b1, b2...))
|
h.Write(bytes)
|
||||||
return fmt.Sprint(h.Sum32()), nil
|
// CSS names can't start with numbers, so prepend a little something
|
||||||
|
return fmt.Sprintf("d2-%d", h.Sum32()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (diagram Diagram) NestedBoundingBox() (topLeft, bottomRight Point) {
|
||||||
|
tl, br := diagram.BoundingBox()
|
||||||
|
for _, d := range diagram.Layers {
|
||||||
|
tl2, br2 := d.NestedBoundingBox()
|
||||||
|
tl.X = go2.Min(tl.X, tl2.X)
|
||||||
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
||||||
|
br.X = go2.Max(br.X, br2.X)
|
||||||
|
br.Y = go2.Max(br.Y, br2.Y)
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Scenarios {
|
||||||
|
tl2, br2 := d.NestedBoundingBox()
|
||||||
|
tl.X = go2.Min(tl.X, tl2.X)
|
||||||
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
||||||
|
br.X = go2.Max(br.X, br2.X)
|
||||||
|
br.Y = go2.Max(br.Y, br2.Y)
|
||||||
|
}
|
||||||
|
for _, d := range diagram.Steps {
|
||||||
|
tl2, br2 := d.NestedBoundingBox()
|
||||||
|
tl.X = go2.Min(tl.X, tl2.X)
|
||||||
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
||||||
|
br.X = go2.Max(br.X, br2.X)
|
||||||
|
br.Y = go2.Max(br.Y, br2.Y)
|
||||||
|
}
|
||||||
|
return tl, br
|
||||||
}
|
}
|
||||||
|
|
||||||
func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
|
func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,42 @@ func TestCLI_E2E(t *testing.T) {
|
||||||
assert.Testdata(t, ".svg", svg)
|
assert.Testdata(t, ".svg", svg)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "animation",
|
||||||
|
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
||||||
|
writeFile(t, dir, "animation.d2", `Chicken's plan: {
|
||||||
|
style.font-size: 35
|
||||||
|
near: top-center
|
||||||
|
shape: text
|
||||||
|
}
|
||||||
|
|
||||||
|
steps: {
|
||||||
|
1: {
|
||||||
|
Approach road
|
||||||
|
}
|
||||||
|
2: {
|
||||||
|
Approach road -> Cross road
|
||||||
|
}
|
||||||
|
3: {
|
||||||
|
Cross road -> Make you wonder why
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
|
||||||
|
assert.Success(t, err)
|
||||||
|
svg := readFile(t, dir, "animation.svg")
|
||||||
|
assert.Testdata(t, ".svg", svg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incompatible-animation",
|
||||||
|
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
||||||
|
writeFile(t, dir, "x.d2", `x -> y`)
|
||||||
|
err := runTestMain(t, ctx, dir, env, "--animate-interval=2", "x.d2", "x.png")
|
||||||
|
assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: -animate-interval can only be used when exporting to SVG.
|
||||||
|
You provided: .png`)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "hello_world_png_sketch",
|
name: "hello_world_png_sketch",
|
||||||
skipCI: true,
|
skipCI: true,
|
||||||
|
|
|
||||||
890
e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |