package logic import ( "bytes" "math" "math/rand" "strconv" "toolbox/pkg/learnnumber/data" "github.com/signintech/gopdf" ) type WritingRequest struct { StartNum int `json:"start_num"` EndNum int `json:"end_num"` PaperSize string `json:"paper_size"` } type WritingNode struct { X, Y, R float64 AngleA, AngleB float64 BulgeR, Dist float64 Number int } func GenerateWritingPDF(req WritingRequest) ([]byte, error) { 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}) err := pdf.AddTTFFontData("basic", data.FontContent) if err != nil { return nil, err } currentNum := req.StartNum // 只要还没达到 EndNum,就继续生成“整页”内容 for currentNum <= req.EndNum { pdf.AddPage() lastPlaced := drawOneFullPage(pdf, currentNum, rect.W, rect.H) currentNum = lastPlaced + 1 } var buf bytes.Buffer _, err = pdf.WriteTo(&buf) return buf.Bytes(), err } // 核心改变:不再受限于 end 参数,而是尝试填满整页 func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int { margin := 80.0 boxW, boxH := pW-2*margin, pH-160.0 xBase, yBase := margin, 100.0 nodeR := 28.0 bulgeR := nodeR * 0.32 dist := nodeR + bulgeR + 18.0 checkR := nodeR * 1.3 var nodes []WritingNode num := start // 无限循环,直到页面塞不下为止 for { placed := false for retry := 0; retry < 1000; retry++ { rx := xBase + dist + 20 + rand.Float64()*(boxW - 2*dist - 40) ry := yBase + dist + 20 + rand.Float64()*(boxH - 2*dist - 40) if isColliding(rx, ry, checkR, nodes) { continue } angleA := rand.Float64() * 2 * math.Pi angleB := angleA + math.Pi + (rand.Float64()-0.5)*1.5 nodes = append(nodes, WritingNode{X: rx, Y: ry, R: nodeR, AngleA: angleA, AngleB: angleB, BulgeR: bulgeR, Dist: dist, Number: num}) placed = true break } if !placed { // 页面已满 break } num++ } // 空间优化 optimizeSpace(nodes, xBase, yBase, boxW, boxH, dist, checkR) // 绘制 for _, n := range nodes { drawFullNode(pdf, n) } // 返回本页最后一个数字 if len(nodes) > 0 { return nodes[len(nodes)-1].Number } return start } func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR float64) { if len(nodes) < 2 { return } for iter := 0; iter < 120; iter++ { for i := range nodes { bestX, bestY := nodes[i].X, nodes[i].Y maxMinDist := calcMinDist(bestX, bestY, i, nodes) for k := 0; k < 8; k++ { moveAngle := rand.Float64() * 2 * math.Pi nx := nodes[i].X + math.Cos(moveAngle)*5.0 ny := nodes[i].Y + math.Sin(moveAngle)*5.0 if nx-dist < xBase || nx+dist > xBase+boxW || ny-dist < yBase || ny+dist > yBase+boxH { continue } if isColliding(nx, ny, checkR, nodes[:i]) || isColliding(nx, ny, checkR, nodes[i+1:]) { continue } newMinDist := calcMinDist(nx, ny, i, nodes) if newMinDist > maxMinDist { maxMinDist = newMinDist; bestX, bestY = nx, ny } } nodes[i].X, nodes[i].Y = bestX, bestY } } } func isColliding(x, y, r float64, existing []WritingNode) bool { for _, e := range existing { d := math.Sqrt(math.Pow(x-e.X, 2) + math.Pow(y-e.Y, 2)) if d < (r + e.R*1.3 + 30.0) { return true } } return false } func calcMinDist(x, y float64, idx int, nodes []WritingNode) float64 { minD := 10000.0 for i, n := range nodes { if i == idx { continue } d := math.Sqrt(math.Pow(x-n.X, 2) + math.Pow(y-n.Y, 2)) if d < minD { minD = d } } return minD } func getEdgePoint(x, y, r, angle float64, isCircle bool) (float64, float64) { gap := 2.0 er := r + gap if isCircle { return x + math.Cos(angle)*er, y + math.Sin(angle)*er } absCos, absSin := math.Abs(math.Cos(angle)), math.Abs(math.Sin(angle)) var d float64 if absCos > absSin { d = er / absCos } else { d = er / absSin } return x + math.Cos(angle)*d, y + math.Sin(angle)*d } func drawCenteredText(pdf *gopdf.GoPdf, cx, cy float64, text string, fontSize float64) { pdf.SetFont("basic", "", fontSize) tw, _ := pdf.MeasureTextWidth(text) pdf.SetXY(cx - tw/2, cy + fontSize*0.38) pdf.Text(text) } func drawFullNode(pdf *gopdf.GoPdf, n WritingNode) { isOdd := n.Number%2 != 0 pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.8) if isOdd { pdf.Oval(n.X-n.R, n.Y-n.R, n.X+n.R, n.Y+n.R) } else { pdf.RectFromUpperLeft(n.X-n.R, n.Y-n.R, n.R*2, n.R*2) } pdf.SetTextColor(220, 220, 220) drawCenteredText(pdf, n.X, n.Y, strconv.Itoa(n.Number), n.R*1.35) pdf.SetStrokeColor(180, 180, 180); pdf.SetLineWidth(1.0) ax, ay := n.X + math.Cos(n.AngleA)*n.Dist, n.Y + math.Sin(n.AngleA)*n.Dist lx1, ly1 := getEdgePoint(n.X, n.Y, n.R, n.AngleA, isOdd) lx2, ly2 := getEdgePoint(ax, ay, n.BulgeR, n.AngleA+math.Pi, !isOdd) pdf.Line(lx1, ly1, lx2, ly2) if isOdd { pdf.RectFromUpperLeft(ax-n.BulgeR, ay-n.BulgeR, n.BulgeR*2, n.BulgeR*2) } else { pdf.Oval(ax-n.BulgeR, ay-n.BulgeR, ax+n.BulgeR, ay+n.BulgeR) } bx, by := n.X + math.Cos(n.AngleB)*n.Dist, n.Y + math.Sin(n.AngleB)*n.Dist lx3, ly3 := getEdgePoint(n.X, n.Y, n.R, n.AngleB, isOdd) lx4, ly4 := getEdgePoint(bx, by, n.BulgeR, n.AngleB+math.Pi, !isOdd) pdf.Line(lx3, ly3, lx4, ly4) if isOdd { pdf.RectFromUpperLeft(bx-n.BulgeR, by-n.BulgeR, n.BulgeR*2, n.BulgeR*2) } else { pdf.Oval(bx-n.BulgeR, by-n.BulgeR, bx+n.BulgeR, by+n.BulgeR) } pdf.SetTextColor(120, 120, 120) drawCenteredText(pdf, bx, by, strconv.Itoa(n.Number+1), math.Max(7, n.BulgeR*1.2)) }