chore(服务端): 初始化工程仓库

This commit is contained in:
leon_mac
2026-10-18 15:11:44 +08:00
commit 13c8bcbf3c
23 changed files with 2543 additions and 0 deletions
+104
View File
@@ -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)
}