initial implementation
This commit is contained in:
parent
41711d4a4e
commit
50bd016e40
3 changed files with 315 additions and 5 deletions
|
|
@ -13,15 +13,17 @@ const PNG exportExtension = ".png"
|
|||
const PPTX exportExtension = ".pptx"
|
||||
const PDF exportExtension = ".pdf"
|
||||
const SVG exportExtension = ".svg"
|
||||
const ASCII exportExtension = ".txt"
|
||||
|
||||
var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}
|
||||
var SUPPORTED_EXTENSIONS = []exportExtension{ASCII, SVG, PNG, PDF, PPTX, GIF}
|
||||
|
||||
var STDOUT_FORMAT_MAP = map[string]exportExtension{
|
||||
"png": PNG,
|
||||
"svg": SVG,
|
||||
"png": PNG,
|
||||
"svg": SVG,
|
||||
"ascii": ASCII,
|
||||
}
|
||||
|
||||
var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg"}
|
||||
var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg", "ascii"}
|
||||
|
||||
func getOutputFormat(stdoutFormatFlag *string, outputPath string) (exportExtension, error) {
|
||||
if stdoutFormatFlag != nil && *stdoutFormatFlag != "" {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2plugin"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2animate"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2ascii"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
|
||||
|
|
@ -103,7 +104,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png). Usage: d2 input.d2 --stdout-format png - > output.png")
|
||||
stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png, ascii). Usage: d2 input.d2 --stdout-format png - > output.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -862,6 +863,22 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
|
|||
}
|
||||
|
||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([]byte, error) {
|
||||
if outputFormat == ASCII {
|
||||
renderOpts := &d2ascii.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Scale: opts.Scale,
|
||||
}
|
||||
ascii, err := d2ascii.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return ascii, err
|
||||
}
|
||||
err = Write(ms, outputPath, ascii)
|
||||
if err != nil {
|
||||
return ascii, err
|
||||
}
|
||||
return ascii, nil
|
||||
}
|
||||
|
||||
toPNG := outputFormat == PNG
|
||||
|
||||
var scale *float64
|
||||
|
|
|
|||
291
d2renderers/d2ascii/d2ascii.go
Normal file
291
d2renderers/d2ascii/d2ascii.go
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
package d2ascii
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
)
|
||||
|
||||
// RenderOpts contains options for ASCII rendering
|
||||
type RenderOpts struct {
|
||||
Pad *int64 // Optional padding around the diagram
|
||||
Scale *float64 // Pixels per ASCII character ratio
|
||||
}
|
||||
|
||||
// Render converts a D2 diagram into ASCII art
|
||||
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||
if opts == nil {
|
||||
opts = &RenderOpts{}
|
||||
}
|
||||
|
||||
// Default padding matching d2svg
|
||||
pad := int(8)
|
||||
if opts.Pad != nil {
|
||||
pad = int(*opts.Pad)
|
||||
}
|
||||
|
||||
// Scale for converting diagram coordinates to ASCII grid
|
||||
// Default: roughly 1 ASCII char = 8x4 pixels
|
||||
scale := struct{ x, y float64 }{8, 4}
|
||||
if opts.Scale != nil {
|
||||
s := *opts.Scale
|
||||
scale.x = s
|
||||
scale.y = s / 2 // Maintain aspect ratio
|
||||
}
|
||||
|
||||
// Calculate canvas dimensions
|
||||
tl, br := diagram.NestedBoundingBox()
|
||||
width := int(math.Ceil(float64(br.X-tl.X+(pad*2)) / scale.x))
|
||||
height := int(math.Ceil(float64(br.Y-tl.Y+(pad*2)) / scale.y))
|
||||
|
||||
// Create ASCII canvas
|
||||
canvas := NewCanvas(width, height)
|
||||
canvas.setScale(scale.x, scale.y)
|
||||
canvas.setOffset(-int(tl.X), -int(tl.Y))
|
||||
canvas.setPad(pad)
|
||||
|
||||
// Draw shapes
|
||||
for _, shape := range diagram.Shapes {
|
||||
err := canvas.drawShape(shape)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Draw connections
|
||||
for _, conn := range diagram.Connections {
|
||||
err := canvas.drawConnection(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return canvas.Bytes(), nil
|
||||
}
|
||||
|
||||
// Canvas handles the ASCII grid and drawing operations
|
||||
type Canvas struct {
|
||||
grid [][]rune
|
||||
w, h int
|
||||
|
||||
// Coordinate transformation
|
||||
scaleX, scaleY float64
|
||||
offsetX, offsetY int
|
||||
pad int
|
||||
}
|
||||
|
||||
func NewCanvas(w, h int) *Canvas {
|
||||
grid := make([][]rune, h)
|
||||
for i := range grid {
|
||||
grid[i] = make([]rune, w)
|
||||
for j := range grid[i] {
|
||||
grid[i][j] = ' '
|
||||
}
|
||||
}
|
||||
return &Canvas{
|
||||
grid: grid,
|
||||
w: w,
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Canvas) setScale(x, y float64) {
|
||||
c.scaleX = x
|
||||
c.scaleY = y
|
||||
}
|
||||
|
||||
func (c *Canvas) setOffset(x, y int) {
|
||||
c.offsetX = x
|
||||
c.offsetY = y
|
||||
}
|
||||
|
||||
func (c *Canvas) setPad(pad int) {
|
||||
c.pad = pad
|
||||
}
|
||||
|
||||
// transformPoint converts diagram coordinates to ASCII grid coordinates
|
||||
func (c *Canvas) transformPoint(x, y int) (int, int) {
|
||||
x = int(float64(x+c.offsetX+c.pad) / c.scaleX)
|
||||
y = int(float64(y+c.offsetY+c.pad) / c.scaleY)
|
||||
return x, y
|
||||
}
|
||||
|
||||
func (c *Canvas) drawShape(shape d2target.Shape) error {
|
||||
x, y := c.transformPoint(int(shape.Pos.X), int(shape.Pos.Y))
|
||||
w := int(float64(shape.Width) / c.scaleX)
|
||||
h := int(float64(shape.Height) / c.scaleY)
|
||||
|
||||
switch shape.Type {
|
||||
case d2target.ShapeCircle:
|
||||
return c.drawCircle(x, y, w, h, shape.Label)
|
||||
case d2target.ShapeSquare:
|
||||
return c.drawRect(x, y, w, h, shape.Label)
|
||||
// Add more shape types as needed
|
||||
default:
|
||||
return c.drawRect(x, y, w, h, shape.Label)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Canvas) drawRect(x, y, w, h int, label string) error {
|
||||
// Draw corners
|
||||
c.set(x, y, '+')
|
||||
c.set(x+w, y, '+')
|
||||
c.set(x, y+h, '+')
|
||||
c.set(x+w, y+h, '+')
|
||||
|
||||
// Draw horizontal edges
|
||||
for i := x + 1; i < x+w; i++ {
|
||||
c.set(i, y, '-')
|
||||
c.set(i, y+h, '-')
|
||||
}
|
||||
|
||||
// Draw vertical edges
|
||||
for i := y + 1; i < y+h; i++ {
|
||||
c.set(x, i, '|')
|
||||
c.set(x+w, i, '|')
|
||||
}
|
||||
|
||||
// Draw label
|
||||
if label != "" {
|
||||
c.drawCenteredText(x+1, y+1, w-1, h-1, label)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Canvas) drawCircle(x, y, w, h int, label string) error {
|
||||
// Approximate circle with ASCII characters
|
||||
c.set(x+w/2, y, '.')
|
||||
c.set(x+w/2, y+h, '\'')
|
||||
c.set(x, y+h/2, '(')
|
||||
c.set(x+w, y+h/2, ')')
|
||||
|
||||
if label != "" {
|
||||
c.drawCenteredText(x+1, y+1, w-1, h-1, label)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Canvas) drawConnection(conn d2target.Connection) error {
|
||||
// Draw a simple line between points for now
|
||||
points := make([]struct{ x, y int }, len(conn.Route))
|
||||
for i, p := range conn.Route {
|
||||
points[i].x, points[i].y = c.transformPoint(int(p.X), int(p.Y))
|
||||
}
|
||||
|
||||
for i := 0; i < len(points)-1; i++ {
|
||||
c.drawLine(points[i].x, points[i].y, points[i+1].x, points[i+1].y)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Canvas) drawLine(x1, y1, x2, y2 int) {
|
||||
// Draw horizontal line
|
||||
if y1 == y2 {
|
||||
for x := min(x1, x2); x <= max(x1, x2); x++ {
|
||||
c.set(x, y1, '-')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Draw vertical line
|
||||
if x1 == x2 {
|
||||
for y := min(y1, y2); y <= max(y1, y2); y++ {
|
||||
c.set(x1, y, '|')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Draw diagonal line
|
||||
dx := abs(x2 - x1)
|
||||
dy := abs(y2 - y1)
|
||||
steep := dy > dx
|
||||
|
||||
if steep {
|
||||
x1, y1 = y1, x1
|
||||
x2, y2 = y2, x2
|
||||
}
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
|
||||
dx = x2 - x1
|
||||
dy = abs(y2 - y1)
|
||||
err := dx / 2
|
||||
ystep := 1
|
||||
if y1 >= y2 {
|
||||
ystep = -1
|
||||
}
|
||||
|
||||
for ; x1 <= x2; x1++ {
|
||||
if steep {
|
||||
c.set(y1, x1, '/')
|
||||
} else {
|
||||
c.set(x1, y1, '/')
|
||||
}
|
||||
err -= dy
|
||||
if err < 0 {
|
||||
y1 += ystep
|
||||
err += dx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Canvas) drawCenteredText(x, y, w, h int, text string) {
|
||||
lines := strings.Split(text, "\n")
|
||||
startY := y + (h-len(lines))/2
|
||||
|
||||
for i, line := range lines {
|
||||
if startY+i >= c.h {
|
||||
break
|
||||
}
|
||||
startX := x + (w-len(line))/2
|
||||
for j, ch := range line {
|
||||
if startX+j >= c.w {
|
||||
break
|
||||
}
|
||||
c.set(startX+j, startY+i, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Canvas) set(x, y int, ch rune) {
|
||||
if x >= 0 && x < c.w && y >= 0 && y < c.h {
|
||||
c.grid[y][x] = ch
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Canvas) Bytes() []byte {
|
||||
var buf bytes.Buffer
|
||||
for _, row := range c.grid {
|
||||
buf.WriteString(string(row))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
Loading…
Reference in a new issue