chore(服务端): 初始化工程仓库
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"html"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"perler-beads-server/internal/domain"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func wrapText(text string, limit int) []string {
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
r := []rune(text)
|
||||
out := []string{}
|
||||
for len(r) > limit {
|
||||
out = append(out, string(r[:limit]))
|
||||
r = r[limit:]
|
||||
}
|
||||
return append(out, string(r))
|
||||
}
|
||||
func renderSVG(w io.Writer, p domain.Pattern, pitch float64) error {
|
||||
width := math.Max(100, float64(p.Width)*pitch+30)
|
||||
titleLines := wrapText(p.Title, int((width-20)/4))
|
||||
gridTop := 20 + float64(len(titleLines)-1)*5
|
||||
height := gridTop + float64(p.Height)*pitch + 70 + float64(len(p.Colors))*5
|
||||
fmt.Fprintf(w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2fmm" height="%.2fmm" viewBox="0 0 %.2f %.2f"><rect width="100%%" height="100%%" fill="white"/><g font-family="sans-serif" fill="#222">`, width, height, width, height)
|
||||
for i, line := range titleLines {
|
||||
fmt.Fprintf(w, `<text x="10" y="%.2f" font-size="4">%s</text>`, 8+float64(i)*5, html.EscapeString(line))
|
||||
}
|
||||
fmt.Fprintf(w, `<text x="10" y="%.2f" font-size="2">%d × %d / %d beads</text>`, gridTop-7, p.Width, p.Height, p.BeadCount)
|
||||
colors := map[string]string{}
|
||||
counts := map[string]int{}
|
||||
for _, c := range p.Colors {
|
||||
colors[c.Code] = c.Hex
|
||||
}
|
||||
for i, v := range p.Cells {
|
||||
x := float64(i%p.Width)*pitch + 15
|
||||
y := float64(i/p.Width)*pitch + gridTop
|
||||
fill := "#fff"
|
||||
label := ""
|
||||
if v != nil {
|
||||
fill = colors[*v]
|
||||
label = *v
|
||||
counts[*v]++
|
||||
}
|
||||
fmt.Fprintf(w, `<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" stroke="#666" stroke-width="0.15"/><text x="%.2f" y="%.2f" font-size="%.2f" text-anchor="middle" fill="black" stroke="white" stroke-width="0.06" paint-order="stroke">%s</text>`, x, y, pitch, pitch, fill, x+pitch/2, y+pitch*.6, math.Min(math.Min(pitch*.2, 1.5), pitch*.85/math.Max(1, float64(len([]rune(label)))*.65)), html.EscapeString(label))
|
||||
}
|
||||
for x := 0; x < p.Width; x++ {
|
||||
fmt.Fprintf(w, `<text x="%.2f" y="%.2f" font-size="1.7">%d</text>`, 15+float64(x)*pitch, gridTop-1, x+1)
|
||||
}
|
||||
for y := 0; y < p.Height; y++ {
|
||||
fmt.Fprintf(w, `<text x="8" y="%.2f" font-size="1.7">%d</text>`, gridTop+float64(y)*pitch+pitch/2, y+1)
|
||||
}
|
||||
baseline := gridTop + float64(p.Height)*pitch + 8
|
||||
fmt.Fprintf(w, `<path d="M15 %.2f h50 m-50 -2 v4 m50 -4 v4" stroke="black" fill="none"/><text x="15" y="%.2f" font-size="3">50 mm calibration</text>`, baseline, baseline+7)
|
||||
keys := []string{}
|
||||
for code := range counts {
|
||||
keys = append(keys, code)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">Print at 100%%. Disable fit-to-page.</text>`, baseline+13)
|
||||
for i, code := range keys {
|
||||
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">%s: %d</text>`, baseline+20+float64(i)*5, html.EscapeString(code), counts[code])
|
||||
}
|
||||
if width > 210 || height > 297 {
|
||||
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">Oversize: use poster/tile mode.</text><text x="15" y="%.2f" font-size="3">Keep 100%% scale and add overlap.</text>`, height-13, height-8)
|
||||
}
|
||||
_, e := fmt.Fprint(w, "</g></svg>")
|
||||
return e
|
||||
}
|
||||
func (s *Server) print(c *gin.Context) {
|
||||
p, yes := s.readPattern(c)
|
||||
if !yes {
|
||||
return
|
||||
}
|
||||
pitch, e := strconv.ParseFloat(c.DefaultQuery("pitchMm", "5"), 64)
|
||||
if e != nil || math.IsNaN(pitch) || math.IsInf(pitch, 0) || pitch < 2 || pitch > 10 {
|
||||
reply(c, 400, "pitchMm 需为 2–10", nil)
|
||||
return
|
||||
}
|
||||
f, e := os.CreateTemp("", "perler-print-*.svg")
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
path := f.Name()
|
||||
defer func() { time.AfterFunc(time.Minute, func() { os.Remove(path) }) }()
|
||||
if e = renderSVG(f, p, pitch); e != nil {
|
||||
f.Close()
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
f.Close()
|
||||
c.Header("Content-Disposition", `attachment; filename="perler-pattern.svg"`)
|
||||
c.Header("Content-Type", "image/svg+xml")
|
||||
c.File(path)
|
||||
}
|
||||
Reference in New Issue
Block a user