169 lines
5.1 KiB
Go
169 lines
5.1 KiB
Go
package logic
|
|
|
|
import (
|
|
"bytes"
|
|
"math"
|
|
"math/rand"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"toolbox/pkg/learnnumber/data"
|
|
|
|
"github.com/signintech/gopdf"
|
|
)
|
|
|
|
type CountingRequest struct {
|
|
TotalCount int `json:"total_count"`
|
|
IconTypes int `json:"icon_types"`
|
|
PaperSize string `json:"paper_size"`
|
|
}
|
|
|
|
var reSVG = regexp.MustCompile(`([MLQCZmlqcz])|(-?\d+\.?\d*)`)
|
|
|
|
func GenerateCountingPDF(req CountingRequest) ([]byte, error) {
|
|
rand.Seed(time.Now().UnixNano())
|
|
pdf := &gopdf.GoPdf{}
|
|
rect := gopdf.Rect{W: 595.28, H: 841.89}
|
|
if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} }
|
|
pdf.Start(gopdf.Config{PageSize: rect})
|
|
pdf.AddPage()
|
|
|
|
drawProblem(pdf, req, 0, rect.W, rect.H/2)
|
|
drawProblem(pdf, req, rect.H/2, rect.W, rect.H/2)
|
|
|
|
var buf bytes.Buffer
|
|
_, err := pdf.WriteTo(&buf)
|
|
return buf.Bytes(), err
|
|
}
|
|
|
|
func drawProblem(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) {
|
|
margin := 40.0
|
|
boxW := (pW - 2*margin) * 0.7
|
|
boxH := pH - 60.0
|
|
x, y := margin, startY + 30.0
|
|
|
|
// 1. 绘制方框
|
|
pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.0)
|
|
pdf.RectFromUpperLeft(x, y, boxW, boxH)
|
|
|
|
// 2. 分配图标数量
|
|
numTypes := req.IconTypes; if numTypes < 1 { numTypes = 1 }
|
|
if numTypes > len(data.CountingIcons) { numTypes = len(data.CountingIcons) }
|
|
|
|
total := req.TotalCount; if total < numTypes { total = numTypes }
|
|
if total > 30 { total = 30 }
|
|
|
|
// 随机选出图标种类
|
|
allIcons := rand.Perm(len(data.CountingIcons))
|
|
selectedIcons := []data.Icon{}
|
|
counts := []int{}
|
|
|
|
// 先每种分配1个
|
|
rem := total - numTypes
|
|
for i := 0; i < numTypes; i++ {
|
|
selectedIcons = append(selectedIcons, data.CountingIcons[allIcons[i]])
|
|
counts = append(counts, 1)
|
|
}
|
|
// 随机分配剩下的
|
|
for i := 0; i < rem; i++ {
|
|
counts[rand.Intn(numTypes)]++
|
|
}
|
|
|
|
// 3. 布局计划 (使用网格,增加 Padding 避开边框)
|
|
rows, cols := 5, 6
|
|
cellW, cellH := boxW/float64(cols), boxH/float64(rows)
|
|
indices := rand.Perm(rows * cols)
|
|
|
|
// 严格图标大小:单元格的 70%,确保不溢出单元格边界
|
|
iconSize := math.Min(cellW, cellH) * 0.7
|
|
paddingX, paddingY := (cellW - iconSize)/2, (cellH - iconSize)/2
|
|
|
|
iconTypeIdx := 0
|
|
countInCurrentType := 0
|
|
|
|
for i := 0; i < total; i++ {
|
|
cellIdx := indices[i]
|
|
rIdx, cIdx := cellIdx/cols, cellIdx%cols
|
|
|
|
// 基础位置
|
|
baseX := x + float64(cIdx)*cellW + paddingX
|
|
baseY := y + float64(rIdx)*cellH + paddingY
|
|
|
|
// 渲染图标
|
|
drawIcon(pdf, selectedIcons[iconTypeIdx], baseX + iconSize/2, baseY + iconSize/2, iconSize)
|
|
|
|
countInCurrentType++
|
|
if countInCurrentType >= counts[iconTypeIdx] {
|
|
iconTypeIdx++
|
|
countInCurrentType = 0
|
|
}
|
|
}
|
|
|
|
// 4. 绘制右侧对照表
|
|
legendX := x + boxW + 20.0
|
|
legendStepY := boxH / float64(numTypes + 1)
|
|
for i := 0; i < numTypes; i++ {
|
|
lY := y + float64(i+1)*legendStepY
|
|
drawIcon(pdf, selectedIcons[i], legendX + 20, lY, 30)
|
|
pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(0.8)
|
|
pdf.RectFromUpperLeft(legendX + 50, lY - 15, 30, 30)
|
|
}
|
|
}
|
|
|
|
func drawIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) {
|
|
scale := size / 1024.0
|
|
pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.5) // 加粗线条
|
|
|
|
ox, oy := cX - size/2, cY - size/2
|
|
for _, pStr := range icon.Paths {
|
|
pts := parseSmoothPath(pStr, scale, ox, oy)
|
|
if len(pts) > 1 {
|
|
// 如果是闭合路径
|
|
if strings.Contains(strings.ToUpper(pStr), "Z") {
|
|
pdf.Polygon(pts, "D")
|
|
} else {
|
|
for i := 0; i < len(pts)-1; i++ {
|
|
pdf.Line(pts[i].X, pts[i].Y, pts[i+1].X, pts[i+1].Y)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 支持贝塞尔曲线采样,确保图标圆润
|
|
func parseSmoothPath(path string, scale, ox, oy float64) []gopdf.Point {
|
|
var pts []gopdf.Point
|
|
matches := reSVG.FindAllStringSubmatch(path, -1)
|
|
var lx, ly float64
|
|
for i := 0; i < len(matches); {
|
|
cmd := matches[i][0]
|
|
if cmd == "M" || cmd == "L" {
|
|
x, _ := strconv.ParseFloat(matches[i+1][0], 64); y, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
|
lx, ly = x, y
|
|
pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
|
|
i += 3
|
|
} else if cmd == "C" {
|
|
x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
|
x2, _ := strconv.ParseFloat(matches[i+3][0], 64); y2, _ := strconv.ParseFloat(matches[i+4][0], 64)
|
|
x, _ := strconv.ParseFloat(matches[i+5][0], 64); y, _ := strconv.ParseFloat(matches[i+6][0], 64)
|
|
for t := 0.25; t <= 1.0; t += 0.25 {
|
|
tx := math.Pow(1-t, 3)*lx + 3*math.Pow(1-t, 2)*t*x1 + 3*(1-t)*math.Pow(t, 2)*x2 + math.Pow(t, 3)*x
|
|
ty := math.Pow(1-t, 3)*ly + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y
|
|
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
|
|
}
|
|
lx, ly = x, y; i += 7
|
|
} else if cmd == "Q" {
|
|
x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
|
x, _ := strconv.ParseFloat(matches[i+3][0], 64); y, _ := strconv.ParseFloat(matches[i+4][0], 64)
|
|
for t := 0.25; t <= 1.0; t += 0.25 {
|
|
tx := math.Pow(1-t, 2)*lx + 2*(1-t)*t*x1 + math.Pow(t, 2)*x
|
|
ty := math.Pow(1-t, 2)*ly + 2*(1-t)*t*y1 + math.Pow(t, 2)*y
|
|
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
|
|
}
|
|
lx, ly = x, y; i += 5
|
|
} else { i++ }
|
|
}
|
|
return pts
|
|
}
|