diff --git a/cmd/toolbox/main.go b/cmd/toolbox/main.go
index ee08f33..8ace6ee 100644
--- a/cmd/toolbox/main.go
+++ b/cmd/toolbox/main.go
@@ -4,6 +4,7 @@ import (
"bytes"
"embed"
"flag"
+ "fmt"
"html/template"
"io/fs"
"log"
@@ -56,6 +57,23 @@ func main() {
subFS, _ := fs.Sub(webFS, "web")
r.StaticFS("/static", http.FS(subFS))
+ // 2.5 SEO 静态文件映射
+ r.GET("/robots.txt", func(c *gin.Context) {
+ c.String(http.StatusOK, "User-agent: *\nAllow: /\nSitemap: https://toolbox.pengzhan.dev/sitemap.xml\n")
+ })
+ r.GET("/sitemap.xml", func(c *gin.Context) {
+ var buf bytes.Buffer
+ buf.WriteString("\n")
+ buf.WriteString("\n")
+ buf.WriteString(" https://toolbox.pengzhan.dev/1.0\n")
+
+ // 动态根据注册的工具生成路由
+ for id := range base.Registry {
+ fmt.Fprintf(&buf, " https://toolbox.pengzhan.dev/%s0.8\n", id)
+ }
+ buf.WriteString("")
+ c.Data(http.StatusOK, "application/xml; charset=utf-8", buf.Bytes())
+ })
// 3. 模板引擎初始化
// 递归加载 web 目录下所有的 .html 文件
tmpl, err := template.ParseFS(subFS, "layout.html", "index.html", "tools/*.html")
@@ -65,11 +83,32 @@ func main() {
// 通用页面渲染函数
serveIndex := func(c *gin.Context) {
- data := gin.H{
- "Title": "Own-Tools",
- "GA_ID": gaID,
- }
+ path := c.Request.URL.Path
+ title := "探索工具箱"
+ desc := "基于 Go 语言构建的模块化个人工具箱,提供汉字字帖生成、数字学习等多种实用生产力工具。"
+ keywords := "个人工具箱, 汉字字帖生成, 书法练习, 数字学习, Own-Tools"
+ baseURL := "https://toolbox.pengzhan.dev"
+ canonical := baseURL + path
+ // 针对不同路径设置 SEO 标签
+ switch path {
+ case "/zitie":
+ title = "汉字字帖生成器 - 教学方格与步进式分解"
+ desc = "在线生成 2x3 教学方格字帖和 9 列步进式笔顺分解字帖,支持多种书法字体和古风排版。"
+ keywords = "字帖生成, 笔顺分解, 汉字教学, 书法字帖, 练字"
+ case "/learn-number":
+ title = "趣味数字学习工具"
+ desc = "为儿童设计的数字学习与计数练习工具,生动活泼,寓教于乐。"
+ keywords = "数字学习, 儿童计数, 幼小衔接"
+ }
+ data := gin.H{
+ "Title": title,
+ "Description": desc,
+ "Keywords": keywords,
+ "CanonicalURL": canonical,
+ "GA_ID": gaID,
+ }
+
var buf bytes.Buffer
// 因为已经 Sub 了,模板名就是文件名
if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil {
@@ -78,12 +117,13 @@ func main() {
}
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
}
-
r.GET("/", serveIndex)
r.GET("/api/tools", func(c *gin.Context) {
var list []map[string]string
ids := make([]string, 0, len(base.Registry))
- for id := range base.Registry { ids = append(ids, id) }
+ for id := range base.Registry {
+ ids = append(ids, id)
+ }
sort.Strings(ids)
for _, id := range ids {
diff --git a/cmd/toolbox/web/layout.html b/cmd/toolbox/web/layout.html
index 9eb3c4d..cf56bf4 100644
--- a/cmd/toolbox/web/layout.html
+++ b/cmd/toolbox/web/layout.html
@@ -4,6 +4,24 @@
{{.Title}} | Own-Tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{if .GA_ID}}
@@ -126,10 +144,41 @@
renderCurrentPath();
}
+ const metaConfig = {
+ 'welcome': {
+ title: '探索工具箱',
+ desc: '基于 Go 语言构建的模块化个人工具箱,提供汉字字帖生成、数字学习等多种实用生产力工具。',
+ keywords: '个人工具箱, 汉字字帖生成, 书法练习, 数字学习, Own-Tools'
+ },
+ 'zitie': {
+ title: '汉字字帖生成器 - 教学方格与步进式分解',
+ desc: '在线生成 2x3 教学方格字帖和 9 列步进式笔顺分解字帖,支持多种书法字体和古风排版。',
+ keywords: '字帖生成, 笔顺分解, 汉字教学, 书法字帖, 练字'
+ },
+ 'learn-number': {
+ title: '趣味数字学习工具',
+ desc: '为儿童设计的数字学习与计数练习工具,生动活泼,寓教于乐。',
+ keywords: '数字学习, 儿童计数, 幼小衔接'
+ }
+ };
+
function renderCurrentPath() {
const path = window.location.pathname.replace('/', '') || 'welcome';
- // 隐藏所有面板
+ // 1. 更新标题与元数据
+ const meta = metaConfig[path] || metaConfig['welcome'];
+ document.title = `${meta.title} | Own-Tools`;
+ document.querySelector('meta[name="description"]').setAttribute('content', meta.desc);
+ document.querySelector('meta[name="keywords"]').setAttribute('content', meta.keywords);
+
+ // 更新 Open Graph 标签 (可选,社交分享主要看服务端渲染)
+ const canonical = `https://toolbox.pengzhan.dev/${path === 'welcome' ? '' : path}`;
+ document.querySelector('link[rel="canonical"]').setAttribute('href', canonical);
+ document.querySelector('meta[property="og:title"]').setAttribute('content', `${meta.title} | Own-Tools`);
+ document.querySelector('meta[property="og:description"]').setAttribute('content', meta.desc);
+ document.querySelector('meta[property="og:url"]').setAttribute('content', canonical);
+
+ // 2. 隐藏所有面板
document.querySelectorAll('.tool-panel').forEach(p => p.classList.remove('active'));
// 显示目标面板
diff --git a/pkg/learnnumber/data/icons.go b/pkg/learnnumber/data/icons.go
index 8fbc153..dbab1c3 100644
--- a/pkg/learnnumber/data/icons.go
+++ b/pkg/learnnumber/data/icons.go
@@ -31,7 +31,7 @@ var IconCategories = map[string][]Icon{
{Name: "Trapezoid", Paths: []string{"M 300 200 L 724 200 L 924 800 L 100 800 Z"}},
{Name: "Hexagon", Paths: []string{"M 512 100 L 858 300 L 858 724 L 512 924 L 166 724 L 166 300 Z"}},
},
- "fruits": {},
+ "fruits": {},
}
type SVG struct {
diff --git a/pkg/learnnumber/logic/counting.go b/pkg/learnnumber/logic/counting.go
index 02bcc8e..2a5fd43 100644
--- a/pkg/learnnumber/logic/counting.go
+++ b/pkg/learnnumber/logic/counting.go
@@ -7,7 +7,6 @@ import (
"regexp"
"strconv"
"strings"
- "time"
"toolbox/pkg/learnnumber/data"
"github.com/signintech/gopdf"
@@ -33,15 +32,20 @@ type PathResult struct {
var reSVGToken = regexp.MustCompile(`[a-zA-Z]|-?\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} }
+ if req.PaperSize == "Letter" {
+ rect = gopdf.Rect{W: 612, H: 792}
+ }
pdf.Start(gopdf.Config{PageSize: rect})
// 硬性限制:页数 1-10 页,防止资源耗尽
- if req.PageCount < 1 { req.PageCount = 1 }
- if req.PageCount > 10 { req.PageCount = 10 }
+ if req.PageCount < 1 {
+ req.PageCount = 1
+ }
+ if req.PageCount > 10 {
+ req.PageCount = 10
+ }
for i := 0; i < req.PageCount; i++ {
pdf.AddPage()
@@ -57,25 +61,40 @@ func GenerateCountingPDF(req CountingRequest) ([]byte, error) {
func drawProblemV4(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) {
margin := 40.0
boxW, boxH := (pW-2*margin)*0.7, pH-60.0
- xBase, yBase := margin, startY + 30.0
+ xBase, yBase := margin, startY+30.0
- pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.5)
+ pdf.SetStrokeColor(0, 0, 0)
+ pdf.SetLineWidth(2.5)
pdf.RectFromUpperLeft(xBase, yBase, boxW, boxH)
catIcons := data.IconCategories[req.Category]
- if len(catIcons) == 0 { catIcons = data.IconCategories["shapes"] }
-
+ if len(catIcons) == 0 {
+ catIcons = data.IconCategories["shapes"]
+ }
+
numTypes := req.IconTypes
- if numTypes < 1 { numTypes = 1 }; if numTypes > 6 { numTypes = 6 }
- if numTypes > len(catIcons) { numTypes = len(catIcons) }
-
+ if numTypes < 1 {
+ numTypes = 1
+ }
+ if numTypes > 6 {
+ numTypes = 6
+ }
+ if numTypes > len(catIcons) {
+ numTypes = len(catIcons)
+ }
+
total := req.TotalCount
- if total < numTypes { total = numTypes }; if total > 30 { total = 30 }
+ if total < numTypes {
+ total = numTypes
+ }
+ if total > 30 {
+ total = 30
+ }
allIconsPerm := rand.Perm(len(catIcons))
var selectedIcons []data.Icon
counts := make([]int, numTypes)
-
+
// 随机分配逻辑
for i := 0; i < numTypes; i++ {
selectedIcons = append(selectedIcons, catIcons[allIconsPerm[i]])
@@ -87,49 +106,80 @@ func drawProblemV4(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64
}
avgRadius := math.Sqrt((boxW * boxH * 0.22) / (float64(total) * math.Pi))
- if avgRadius > 35.0 { avgRadius = 35.0 }
+ if avgRadius > 35.0 {
+ avgRadius = 35.0
+ }
var placed []PlacedIcon
iconTypeIdx, currentInType := 0, 0
for i := 0; i < total; i++ {
- scaleVar := 0.9 + rand.Float64()*0.2; r := avgRadius * scaleVar
+ scaleVar := 0.9 + rand.Float64()*0.2
+ r := avgRadius * scaleVar
for retry := 0; retry < 200; retry++ {
randX := xBase + r + 5 + rand.Float64()*(boxW-2*r-10)
randY := yBase + r + 5 + rand.Float64()*(boxH-2*r-10)
collision := false
- for _, p := range placed {
- if math.Sqrt(math.Pow(randX-p.X, 2)+math.Pow(randY-p.Y, 2)) < (r + p.Radius + 10.0) {
- collision = true; break
- }
- }
+ for _, p := range placed {
+ dx := randX - p.X
+ dy := randY - p.Y
+ if math.Sqrt(dx*dx+dy*dy) < (r + p.Radius + 10.0) {
+ collision = true
+ break
+ }
+ }
+
if !collision {
drawSimpleIcon(pdf, selectedIcons[iconTypeIdx], randX, randY, r*2)
- placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r}); break
+ placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r})
+ break
}
}
- currentInType++; if currentInType >= counts[iconTypeIdx] { iconTypeIdx++; currentInType = 0 }
+ currentInType++
+ if currentInType >= counts[iconTypeIdx] {
+ iconTypeIdx++
+ currentInType = 0
+ }
}
legendX, legendStepY := xBase+boxW+20.0, boxH/float64(numTypes+1)
for i := 0; i < numTypes; i++ {
lY := yBase + float64(i+1)*legendStepY
drawSimpleIcon(pdf, selectedIcons[i], legendX+25, lY, 35)
- pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(1.0); pdf.RectFromUpperLeft(legendX+60, lY-15, 35, 35)
+ pdf.SetStrokeColor(150, 150, 150)
+ pdf.SetLineWidth(1.0)
+ pdf.RectFromUpperLeft(legendX+60, lY-15, 35, 35)
}
}
func drawSimpleIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) {
rawResults := parseMultiPath(icon, 1.0, 0, 0)
- if len(rawResults) == 0 { return }
- var minX, minY, maxX, maxY float64 = 1e9, 1e9, -1e9, -1e9
+ if len(rawResults) == 0 {
+ return
+ }
+ var minX, minY, maxX, maxY = 1e9, 1e9, -1e9, -1e9
for _, res := range rawResults {
for _, p := range res.Points {
- if p.X < minX { minX = p.X }; if p.X > maxX { maxX = p.X }
- if p.Y < minY { minY = p.Y }; if p.Y > maxY { maxY = p.Y }
+ if p.X < minX {
+ minX = p.X
+ }
+ if p.X > maxX {
+ maxX = p.X
+ }
+ if p.Y < minY {
+ minY = p.Y
+ }
+ if p.Y > maxY {
+ maxY = p.Y
+ }
}
}
curW, curH := maxX-minX, maxY-minY
- if curW <= 0 { curW = 1 }; if curH <= 0 { curH = 1 }
+ if curW <= 0 {
+ curW = 1
+ }
+ if curH <= 0 {
+ curH = 1
+ }
scale := size / math.Max(curW, curH)
ox, oy := cX-(curW*scale)/2-minX*scale, cY-(curH*scale)/2-minY*scale
@@ -141,10 +191,16 @@ func drawSimpleIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) {
}
for _, res := range rawResults {
scaledPts := make([]gopdf.Point, len(res.Points))
- for i, p := range res.Points { scaledPts[i] = gopdf.Point{X: ox + p.X*scale, Y: oy + p.Y*scale} }
+ for i, p := range res.Points {
+ scaledPts[i] = gopdf.Point{X: ox + p.X*scale, Y: oy + p.Y*scale}
+ }
if len(scaledPts) > 1 {
- if res.Closed { pdf.Polygon(scaledPts, "D") } else {
- for j := 0; j < len(scaledPts)-1; j++ { pdf.Line(scaledPts[j].X, scaledPts[j].Y, scaledPts[j+1].X, scaledPts[j+1].Y) }
+ if res.Closed {
+ pdf.Polygon(scaledPts, "D")
+ } else {
+ for j := 0; j < len(scaledPts)-1; j++ {
+ pdf.Line(scaledPts[j].X, scaledPts[j].Y, scaledPts[j+1].X, scaledPts[j+1].Y)
+ }
}
}
}
@@ -163,57 +219,122 @@ func parseMultiPath(icon data.Icon, scale, ox, oy float64) []PathResult {
if (token[0] >= 'a' && token[0] <= 'z') || (token[0] >= 'A' && token[0] <= 'Z') {
cmd := strings.ToUpper(token)
if cmd == "M" {
- if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}); pts = nil }
+ if len(pts) > 0 {
+ results = append(results, PathResult{Points: pts, Closed: false})
+ pts = nil
+ }
} else if cmd == "Z" {
- if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: true}); pts = nil }
- lx, ly = startX, startY; i++; continue
+ if len(pts) > 0 {
+ results = append(results, PathResult{Points: pts, Closed: true})
+ pts = nil
+ }
+ lx, ly = startX, startY
+ i++
+ continue
}
- currentCmd = cmd; isRel = (token[0] >= 'a' && token[0] <= 'z'); i++
+ currentCmd = cmd
+ isRel = (token[0] >= 'a' && token[0] <= 'z')
+ i++
continue
}
switch currentCmd {
case "M", "L":
- if i+1 >= len(tokens) { i = len(tokens); break }
- x, _ := strconv.ParseFloat(tokens[i], 64); y, _ := strconv.ParseFloat(tokens[i+1], 64)
- if isRel { x += lx; y += ly }
- if currentCmd == "M" { startX, startY = x, y }
+ if i+1 >= len(tokens) {
+ i = len(tokens)
+ break
+ }
+ x, _ := strconv.ParseFloat(tokens[i], 64)
+ y, _ := strconv.ParseFloat(tokens[i+1], 64)
+ if isRel {
+ x += lx
+ y += ly
+ }
+ if currentCmd == "M" {
+ startX, startY = x, y
+ }
lx, ly = x, y
- pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}); i += 2
- if currentCmd == "M" { currentCmd = "L" }
+ pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
+ i += 2
+ if currentCmd == "M" {
+ currentCmd = "L"
+ }
case "H":
- x, _ := strconv.ParseFloat(tokens[i], 64); if isRel { x += lx }; lx = x
- pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + ly*scale}); i++
+ x, _ := strconv.ParseFloat(tokens[i], 64)
+ if isRel {
+ x += lx
+ }
+ lx = x
+ pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + ly*scale})
+ i++
case "V":
- y, _ := strconv.ParseFloat(tokens[i], 64); if isRel { y += ly }; ly = y
- pts = append(pts, gopdf.Point{X: ox + lx*scale, Y: oy + y*scale}); i++
+ y, _ := strconv.ParseFloat(tokens[i], 64)
+ if isRel {
+ y += ly
+ }
+ ly = y
+ pts = append(pts, gopdf.Point{X: ox + lx*scale, Y: oy + y*scale})
+ i++
case "C":
- if i+5 >= len(tokens) { i = len(tokens); break }
- x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64)
- x2, _ := strconv.ParseFloat(tokens[i+2], 64); y2, _ := strconv.ParseFloat(tokens[i+3], 64)
- x, _ := strconv.ParseFloat(tokens[i+4], 64); y, _ := strconv.ParseFloat(tokens[i+5], 64)
- if isRel { x1 += lx; y1 += ly; x2 += lx; y2 += ly; x += lx; y += ly }
- for t := 0.2; t <= 1.0; t += 0.2 {
- 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})
+ if i+5 >= len(tokens) {
+ i = len(tokens)
+ break
}
- lx, ly = x, y; i += 6
+ x1, _ := strconv.ParseFloat(tokens[i], 64)
+ y1, _ := strconv.ParseFloat(tokens[i+1], 64)
+ x2, _ := strconv.ParseFloat(tokens[i+2], 64)
+ y2, _ := strconv.ParseFloat(tokens[i+3], 64)
+ x, _ := strconv.ParseFloat(tokens[i+4], 64)
+ y, _ := strconv.ParseFloat(tokens[i+5], 64)
+ if isRel {
+ x1 += lx
+ y1 += ly
+ x2 += lx
+ y2 += ly
+ x += lx
+ y += ly
+ }
+ for t := 0.2; t <= 1.0; t += 0.2 {
+ invT := 1 - t
+ tx := invT*invT*invT*lx + 3*invT*invT*t*x1 + 3*invT*t*t*x2 + t*t*t*x
+ ty := invT*invT*invT*ly + 3*invT*invT*t*y1 + 3*invT*t*t*y2 + t*t*t*y
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
+ }
+
+ lx, ly = x, y
+ i += 6
case "Q":
- if i+3 >= len(tokens) { i = len(tokens); break }
- x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64)
- x, _ := strconv.ParseFloat(tokens[i+2], 64); y, _ := strconv.ParseFloat(tokens[i+3], 64)
- if isRel { x1 += lx; y1 += ly; x += lx; y += ly }
- 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})
+ if i+3 >= len(tokens) {
+ i = len(tokens)
+ break
}
- lx, ly = x, y; i += 4
- case "A": i += 7
- default: i++
+ x1, _ := strconv.ParseFloat(tokens[i], 64)
+ y1, _ := strconv.ParseFloat(tokens[i+1], 64)
+ x, _ := strconv.ParseFloat(tokens[i+2], 64)
+ y, _ := strconv.ParseFloat(tokens[i+3], 64)
+ if isRel {
+ x1 += lx
+ y1 += ly
+ x += lx
+ y += ly
+ }
+ for t := 0.25; t <= 1.0; t += 0.25 {
+ invT := 1 - t
+ tx := invT*invT*lx + 2*invT*t*x1 + t*t*x
+ ty := invT*invT*ly + 2*invT*t*y1 + t*t*y
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
+ }
+
+ lx, ly = x, y
+ i += 4
+ case "A":
+ i += 7
+ default:
+ i++
}
}
- if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}) }
+ if len(pts) > 0 {
+ results = append(results, PathResult{Points: pts, Closed: false})
+ }
}
return results
}
diff --git a/pkg/learnnumber/logic/writing.go b/pkg/learnnumber/logic/writing.go
index 10fe864..84934d5 100644
--- a/pkg/learnnumber/logic/writing.go
+++ b/pkg/learnnumber/logic/writing.go
@@ -17,7 +17,7 @@ type WritingRequest struct {
}
type WritingNode struct {
- X, Y, R float64
+ X, Y, R float64
AngleA, AngleB float64
BulgeR, Dist float64
Number int
@@ -26,11 +26,15 @@ type WritingNode struct {
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} }
+ 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 }
+ if err != nil {
+ return nil, err
+ }
currentNum := req.StartNum
// 只要还没达到 EndNum,就继续生成“整页”内容
@@ -54,7 +58,7 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int {
nodeR := 28.0
bulgeR := nodeR * 0.32
dist := nodeR + bulgeR + 18.0
- checkR := nodeR * 1.3
+ checkR := nodeR * 1.3
var nodes []WritingNode
num := start
@@ -63,11 +67,13 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int {
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 }
-
+ 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})
@@ -98,7 +104,9 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int {
}
func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR float64) {
- if len(nodes) < 2 { return }
+ if len(nodes) < 2 {
+ return
+ }
for iter := 0; iter < 120; iter++ {
for i := range nodes {
bestX, bestY := nodes[i].X, nodes[i].Y
@@ -107,11 +115,16 @@ func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR f
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 }
+ 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
+ maxMinDist = newMinDist
+ bestX, bestY = nx, ny
}
}
nodes[i].X, nodes[i].Y = bestX, bestY
@@ -121,8 +134,12 @@ func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR f
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 }
+ dx := x - e.X
+ dy := y - e.Y
+ d := math.Sqrt(dx*dx + dy*dy)
+ if d < (r + e.R*1.3 + 30.0) {
+ return true
+ }
}
return false
}
@@ -130,50 +147,76 @@ func isColliding(x, y, r float64, existing []WritingNode) bool {
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 }
+ if i == idx {
+ continue
+ }
+ dx := x - n.X
+ dy := y - n.Y
+ d := math.Sqrt(dx*dx + dy*dy)
+ if d < minD {
+ minD = d
+ }
}
return minD
}
func getEdgePoint(x, y, r, angle float64, isCircle bool) (float64, float64) {
- gap := 2.0
+ gap := 2.0
er := r + gap
- if isCircle { return x + math.Cos(angle)*er, y + math.Sin(angle)*er }
+ 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 }
+ 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)
+ _ = pdf.SetFont("basic", "", fontSize)
tw, _ := pdf.MeasureTextWidth(text)
- pdf.SetXY(cx - tw/2, cy + fontSize*0.38)
- pdf.Text(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.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
+ 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) }
+ 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
+ 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) }
-
+ 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))
}
diff --git a/pkg/learnnumber/tool.go b/pkg/learnnumber/tool.go
index 3c2506e..d11ecd0 100644
--- a/pkg/learnnumber/tool.go
+++ b/pkg/learnnumber/tool.go
@@ -16,10 +16,12 @@ func init() {
base.Register(&learnNumberTool{})
}
-func (t *learnNumberTool) ID() string { return "learn-number" }
-func (t *learnNumberTool) Name() string { return "幼儿数学助手" }
-func (t *learnNumberTool) Description() string { return "包含数图形、基础加减法等趣味数学练习" }
-func (t *learnNumberTool) Emoji() string { return "🔢" }
+func (t *learnNumberTool) ID() string { return "learn-number" }
+func (t *learnNumberTool) Name() string { return "幼儿数学助手" }
+func (t *learnNumberTool) Description() string {
+ return "包含数图形、基础加减法等趣味数学练习"
+}
+func (t *learnNumberTool) Emoji() string { return "🔢" }
func (t *learnNumberTool) Init() error {
fmt.Println("Initializing Learn-Number tool...")
diff --git a/pkg/zitie/logic/pdf.go b/pkg/zitie/logic/pdf.go
index 91b348a..69e553d 100644
--- a/pkg/zitie/logic/pdf.go
+++ b/pkg/zitie/logic/pdf.go
@@ -16,14 +16,16 @@ var (
sfntMap = make(map[string]*sfnt.Font)
)
-func SetFontData(data []byte) { embeddedFont = data }
-func RegisterFontPath(id, path string) { fontMap[id] = path }
+func SetFontData(data []byte) { embeddedFont = data }
+func RegisterFontPath(id, path string) { fontMap[id] = path }
func RegisterFontSFNT(id string, f *sfnt.Font) { sfntMap[id] = f }
// HasGlyph 检查字体是否包含某个字符
func HasGlyph(fontId string, char rune) bool {
f, ok := sfntMap[fontId]
- if !ok { return false }
+ if !ok {
+ return false
+ }
var buffer sfnt.Buffer
idx, err := f.GlyphIndex(&buffer, char)
return err == nil && idx != 0
@@ -33,10 +35,14 @@ func HasGlyph(fontId string, char rune) bool {
func GeneratePDFExtended(chars []HanziData, mode, paperSize, fontId string) ([]byte, error) {
pdf := &gopdf.GoPdf{}
rect := gopdf.Rect{W: 595.28, H: 841.89}
- if paperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} }
+ if paperSize == "Letter" {
+ rect = gopdf.Rect{W: 612, H: 792}
+ }
pdf.Start(gopdf.Config{PageSize: rect})
- if len(embeddedFont) > 0 { _ = pdf.AddTTFFontData("font", embeddedFont) }
+ if len(embeddedFont) > 0 {
+ _ = pdf.AddTTFFontData("font", embeddedFont)
+ }
// 注册所有可用字体
for id, path := range fontMap {
@@ -58,13 +64,17 @@ func GeneratePDFExtended(chars []HanziData, mode, paperSize, fontId string) ([]b
}
func addTeachingPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH float64) {
- cols := 2; gap, margin := 17.0, 40.0
+ cols := 2
+ gap, margin := 17.0, 40.0
size := math.Min((pW-2*margin-gap)/2, (pH-2*margin-2*gap)/3)
totalW, totalH := 2*size+gap, 3*size+2*gap
marginX, marginY := (pW-totalW)/2, (pH-totalH)/2
for i := 0; i < len(chars); i += 6 {
pdf.AddPage()
- end := i + 6; if end > len(chars) { end = len(chars) }
+ end := i + 6
+ if end > len(chars) {
+ end = len(chars)
+ }
for idx, data := range chars[i:end] {
drawCharacter(pdf, data, marginX+float64(idx%cols)*(size+gap), marginY+float64(idx/cols)*(size+gap), size, flipY, len(data.Strokes), true)
}
@@ -72,58 +82,91 @@ func addTeachingPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH fl
}
func addStepPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH float64) {
- cols, margin := 9, 30.0; size := (pW - 2*margin) / float64(cols)
- pdf.AddPage(); currY := margin
+ cols, margin := 9, 30.0
+ size := (pW - 2*margin) / float64(cols)
+ pdf.AddPage()
+ currY := margin
for _, data := range chars {
- numS := len(data.Strokes); rowsN := 1; if numS > 8 { rowsN = 1 + (numS - 8 + 7) / 8 }
- if currY + float64(rowsN)*size > pH - margin { pdf.AddPage(); currY = margin }
- totalG := rowsN * cols; strokeC := 0
+ numS := len(data.Strokes)
+ rowsN := 1
+ if numS > 8 {
+ rowsN = 1 + (numS-8+7)/8
+ }
+ if currY+float64(rowsN)*size > pH-margin {
+ pdf.AddPage()
+ currY = margin
+ }
+ totalG := rowsN * cols
+ strokeC := 0
for gIdx := 0; gIdx < totalG; gIdx++ {
- r, c := gIdx/cols, gIdx%cols; x, y := margin+float64(c)*size, currY+float64(r)*size
- if r == 0 && c == 0 { drawCharacter(pdf, data, x, y, size, flipY, len(data.Strokes), false); strokeC = 1
- } else if r > 0 && c == 0 { drawMiZiGe(pdf, x, y, size)
- } else if strokeC <= numS { drawStepBox(pdf, data, x, y, size, flipY, strokeC); strokeC++
- } else { drawMiZiGe(pdf, x, y, size) }
+ r, c := gIdx/cols, gIdx%cols
+ x, y := margin+float64(c)*size, currY+float64(r)*size
+ if r == 0 && c == 0 {
+ drawCharacter(pdf, data, x, y, size, flipY, len(data.Strokes), false)
+ strokeC = 1
+ } else if r > 0 && c == 0 {
+ drawMiZiGe(pdf, x, y, size)
+ } else if strokeC <= numS {
+ drawStepBox(pdf, data, x, y, size, flipY, strokeC)
+ strokeC++
+ } else {
+ drawMiZiGe(pdf, x, y, size)
+ }
}
currY += float64(rowsN) * size
}
}
func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, targetFont string) {
- margin := 40.0; cols, rows := 10, 15
+ margin := 40.0
+ cols, rows := 10, 15
colW, rowH := (pW-2*margin)/float64(cols), (pH-2*margin)/float64(rows)
fontSize := colW * 0.75
- pdf.AddPage(); drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
+ pdf.AddPage()
+ drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
cIdx, rIdx := 0, 0
-
+
for _, data := range chars {
if data.Character == "\n" {
- cIdx++; rIdx = 0
- if cIdx >= cols { pdf.AddPage(); cIdx = 0; drawManuscriptGrid(pdf, pW, pH, margin, cols, colW) }
+ cIdx++
+ rIdx = 0
+ if cIdx >= cols {
+ pdf.AddPage()
+ cIdx = 0
+ drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
+ }
continue
}
if rIdx >= rows {
- cIdx++; rIdx = 0
- if cIdx >= cols { pdf.AddPage(); cIdx = 0; drawManuscriptGrid(pdf, pW, pH, margin, cols, colW) }
+ cIdx++
+ rIdx = 0
+ if cIdx >= cols {
+ pdf.AddPage()
+ cIdx = 0
+ drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
+ }
}
- x, y := pW - margin - float64(cIdx+1)*colW, margin + float64(rIdx)*rowH
+ x, y := pW-margin-float64(cIdx+1)*colW, margin+float64(rIdx)*rowH
charRune := []rune(data.Character)[0]
// 智能字体选择
renderFont := targetFont
isFallback := false
-
+
if !HasGlyph(targetFont, charRune) {
// 尝试降级到字库最全的宋体或楷体
if HasGlyph("songti", charRune) {
- renderFont = "songti"; isFallback = true
+ renderFont = "songti"
+ isFallback = true
} else if HasGlyph("kaiti", charRune) {
- renderFont = "kaiti"; isFallback = true
+ renderFont = "kaiti"
+ isFallback = true
} else {
// 全部缺失,保留空白
- rIdx++; continue
+ rIdx++
+ continue
}
}
@@ -133,12 +176,12 @@ func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, tar
smallSize := fontSize * 0.5
_ = pdf.SetFont(renderFont, "", smallSize)
// 计算右上角位置:X靠右,Y靠上
- pdf.SetXY(x + colW - smallSize - 2, y + 2)
+ pdf.SetXY(x+colW-smallSize-2, y+2)
_ = pdf.Cell(nil, data.Character)
} else {
// 正常显示
_ = pdf.SetFont(renderFont, "", fontSize)
- pdf.SetXY(x + (colW-fontSize*0.9)/2, y + (rowH-fontSize)/2)
+ pdf.SetXY(x+(colW-fontSize*0.9)/2, y+(rowH-fontSize)/2)
_ = pdf.Cell(nil, data.Character)
}
rIdx++
@@ -146,108 +189,221 @@ func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, tar
}
func drawManuscriptGrid(pdf *gopdf.GoPdf, pW, pH, margin float64, cols int, colW float64) {
- pdf.SetStrokeColor(200, 0, 0); pdf.SetLineWidth(0.6)
- for c := 0; c <= cols; c++ { x := pW - margin - float64(c)*colW; pdf.Line(x, margin, x, pH-margin) }
- pdf.Line(margin, margin, pW-margin, margin); pdf.Line(margin, pH-margin, pW-margin, pH-margin)
+ pdf.SetStrokeColor(200, 0, 0)
+ pdf.SetLineWidth(0.6)
+ for c := 0; c <= cols; c++ {
+ x := pW - margin - float64(c)*colW
+ pdf.Line(x, margin, x, pH-margin)
+ }
+ pdf.Line(margin, margin, pW-margin, margin)
+ pdf.Line(margin, pH-margin, pW-margin, pH-margin)
}
func drawCharacter(pdf *gopdf.GoPdf, data HanziData, x, y, size float64, flipY bool, strokeLimit int, showAnnotations bool) {
- if showAnnotations { drawGrid(pdf, x, y, size) } else { drawMiZiGe(pdf, x, y, size) }
- p, drawS := size*0.12, size-(size*0.12*2); scale := drawS/1024.0
+ if showAnnotations {
+ drawGrid(pdf, x, y, size)
+ } else {
+ drawMiZiGe(pdf, x, y, size)
+ }
+ p, drawS := size*0.12, size-(size*0.12*2)
+ scale := drawS / 1024.0
if strokeLimit == len(data.Strokes) {
- if showAnnotations { pdf.SetFillColor(240, 240, 240) } else { pdf.SetFillColor(0, 0, 0) }
- } else { pdf.SetFillColor(180, 180, 180) }
+ if showAnnotations {
+ pdf.SetFillColor(240, 240, 240)
+ } else {
+ pdf.SetFillColor(0, 0, 0)
+ }
+ } else {
+ pdf.SetFillColor(180, 180, 180)
+ }
for sIdx := 0; sIdx < len(data.Strokes); sIdx++ {
- if sIdx >= strokeLimit && strokeLimit != len(data.Strokes) { break }
+ if sIdx >= strokeLimit && strokeLimit != len(data.Strokes) {
+ break
+ }
pts := parseSVGPath(data.Strokes[sIdx], scale, x+p, y+p, flipY)
- if len(pts) > 2 { pdf.Polygon(pts, "F") }
+ if len(pts) > 2 {
+ pdf.Polygon(pts, "F")
+ }
}
if showAnnotations && strokeLimit == len(data.Strokes) {
fS := size * 0.035
_ = pdf.SetFont("font", "", fS)
for idx, median := range data.Medians {
- mP := transformPoints(median, scale, x+p, y+p, flipY); if len(mP) < 2 { continue }
- pdf.SetStrokeColor(255, 0, 0); pdf.SetLineWidth(0.6)
- for j := 0; j < len(mP)-1; j++ { pdf.Line(mP[j].X, mP[j].Y, mP[j+1].X, mP[j+1].Y) }
- pdf.SetFillColor(255, 0, 0); drawArrow(pdf, mP[len(mP)-1], mP[len(mP)-2], size*0.035)
- r := size*0.025; dx, dy := mP[1].X-mP[0].X, mP[1].Y-mP[0].Y; dist := math.Sqrt(dx*dx+dy*dy); ux, uy := -1.0, -1.0
- if dist > 0.001 { ux, uy = dx/dist, dy/dist }; cX, cY := mP[0].X-ux*r, mP[0].Y-uy*r
- pdf.SetStrokeColor(255, 0, 0); pdf.SetLineWidth(0.5); pdf.Oval(cX-r, cY-r, cX+r, cY+r)
- numS := strconv.Itoa(idx+1); tw := fS*0.6*float64(len(numS))/2; if len(numS) > 1 { tw = fS*0.5 }
- pdf.SetFillColor(255, 0, 0); pdf.SetXY(cX-tw/2, cY-fS/2); _ = pdf.Cell(nil, numS)
+ mP := transformPoints(median, scale, x+p, y+p, flipY)
+ if len(mP) < 2 {
+ continue
+ }
+ pdf.SetStrokeColor(255, 0, 0)
+ pdf.SetLineWidth(0.6)
+ for j := 0; j < len(mP)-1; j++ {
+ pdf.Line(mP[j].X, mP[j].Y, mP[j+1].X, mP[j+1].Y)
+ }
+ pdf.SetFillColor(255, 0, 0)
+ drawArrow(pdf, mP[len(mP)-1], mP[len(mP)-2], size*0.035)
+ r := size * 0.025
+ dx, dy := mP[1].X-mP[0].X, mP[1].Y-mP[0].Y
+ dist := math.Sqrt(dx*dx + dy*dy)
+ ux, uy := -1.0, -1.0
+ if dist > 0.001 {
+ ux, uy = dx/dist, dy/dist
+ }
+ cX, cY := mP[0].X-ux*r, mP[0].Y-uy*r
+ pdf.SetStrokeColor(255, 0, 0)
+ pdf.SetLineWidth(0.5)
+ pdf.Oval(cX-r, cY-r, cX+r, cY+r)
+ numS := strconv.Itoa(idx + 1)
+ tw := fS * 0.6 * float64(len(numS)) / 2
+ if len(numS) > 1 {
+ tw = fS * 0.5
+ }
+ pdf.SetFillColor(255, 0, 0)
+ pdf.SetXY(cX-tw/2, cY-fS/2)
+ _ = pdf.Cell(nil, numS)
}
}
}
func drawStepBox(pdf *gopdf.GoPdf, data HanziData, x, y, size float64, flipY bool, limit int) {
- drawMiZiGe(pdf, x, y, size); p, scale := size*0.12, (size-(size*0.12*2))/1024.0
- pdf.SetFillColor(180, 180, 180); for i := 0; i < limit; i++ {
- pts := parseSVGPath(data.Strokes[i], scale, x+p, y+p, flipY); if len(pts) > 2 { pdf.Polygon(pts, "F") }
+ drawMiZiGe(pdf, x, y, size)
+ p, scale := size*0.12, (size-(size*0.12*2))/1024.0
+ pdf.SetFillColor(180, 180, 180)
+ for i := 0; i < limit; i++ {
+ pts := parseSVGPath(data.Strokes[i], scale, x+p, y+p, flipY)
+ if len(pts) > 2 {
+ pdf.Polygon(pts, "F")
+ }
}
}
func drawGrid(pdf *gopdf.GoPdf, x, y, size float64) {
- pdf.SetStrokeColor(220, 220, 220); pdf.SetLineWidth(0.4); pdf.RectFromUpperLeft(x, y, size, size)
- mid := size/2; drawDashedLine(pdf, x, y+mid, x+size, y+mid); drawDashedLine(pdf, x+mid, y, x+mid, y+size)
+ pdf.SetStrokeColor(220, 220, 220)
+ pdf.SetLineWidth(0.4)
+ pdf.RectFromUpperLeft(x, y, size, size)
+ mid := size / 2
+ drawDashedLine(pdf, x, y+mid, x+size, y+mid)
+ drawDashedLine(pdf, x+mid, y, x+mid, y+size)
}
func drawMiZiGe(pdf *gopdf.GoPdf, x, y, size float64) {
- pdf.SetStrokeColor(220, 220, 220); pdf.SetLineWidth(0.4); pdf.RectFromUpperLeft(x, y, size, size)
- mid := size/2; drawDashedLine(pdf, x, y+mid, x+size, y+mid); drawDashedLine(pdf, x+mid, y, x+mid, y+size); drawDashedLine(pdf, x, y, x+size, y+size); drawDashedLine(pdf, x, y+size, x+size, y)
+ pdf.SetStrokeColor(220, 220, 220)
+ pdf.SetLineWidth(0.4)
+ pdf.RectFromUpperLeft(x, y, size, size)
+ mid := size / 2
+ drawDashedLine(pdf, x, y+mid, x+size, y+mid)
+ drawDashedLine(pdf, x+mid, y, x+mid, y+size)
+ drawDashedLine(pdf, x, y, x+size, y+size)
+ drawDashedLine(pdf, x, y+size, x+size, y)
}
func drawDashedLine(pdf *gopdf.GoPdf, x1, y1, x2, y2 float64) {
- dash := 2.0; dx, dy := x2-x1, y2-y1; dist := math.Sqrt(dx*dx + dy*dy); if dist < 0.001 { return }
- ux, uy := dx/dist, dy/dist; for i := 0.0; i < dist; i += dash * 2 { end := i + dash; if end > dist { end = dist }; pdf.Line(x1+ux*i, y1+uy*i, x1+ux*end, y1+uy*end) }
+ dash := 2.0
+ dx, dy := x2-x1, y2-y1
+ dist := math.Sqrt(dx*dx + dy*dy)
+ if dist < 0.001 {
+ return
+ }
+ ux, uy := dx/dist, dy/dist
+ for i := 0.0; i < dist; i += dash * 2 {
+ end := i + dash
+ if end > dist {
+ end = dist
+ }
+ pdf.Line(x1+ux*i, y1+uy*i, x1+ux*end, y1+uy*end)
+ }
}
func drawArrow(pdf *gopdf.GoPdf, target, prev gopdf.Point, length float64) {
- angle := math.Atan2(target.Y-prev.Y, target.X-prev.X); arrowA := math.Pi/6
- p1X := target.X+length*math.Cos(angle+math.Pi+arrowA); p1Y := target.Y+length*math.Sin(angle+math.Pi+arrowA)
- p2X := target.X+length*math.Cos(angle+math.Pi-arrowA); p2Y := target.Y+length*math.Sin(angle+math.Pi-arrowA)
+ angle := math.Atan2(target.Y-prev.Y, target.X-prev.X)
+ arrowA := math.Pi / 6
+ p1X := target.X + length*math.Cos(angle+math.Pi+arrowA)
+ p1Y := target.Y + length*math.Sin(angle+math.Pi+arrowA)
+ p2X := target.X + length*math.Cos(angle+math.Pi-arrowA)
+ p2Y := target.Y + length*math.Sin(angle+math.Pi-arrowA)
pdf.Polygon([]gopdf.Point{target, {X: p1X, Y: p1Y}, {X: p2X, Y: p2Y}}, "F")
}
func transformPoints(pts []Point, scale, ox, oy float64, flipY bool) []gopdf.Point {
- res := make([]gopdf.Point, len(pts)); for i, p := range pts { y := p[1]; if flipY { y = 1024-y }; res[i] = gopdf.Point{X: ox+p[0]*scale, Y: oy+y*scale} }
+ res := make([]gopdf.Point, len(pts))
+ for i, p := range pts {
+ y := p[1]
+ if flipY {
+ y = 1024 - y
+ }
+ res[i] = gopdf.Point{X: ox + p[0]*scale, Y: oy + y*scale}
+ }
return res
}
var reSVG = regexp.MustCompile(`([MLQCZ])|(-?\d+\.?\d*)`)
func parseSVGPath(path string, scale, ox, oy float64, flipY bool) []gopdf.Point {
- var pts []gopdf.Point; matches := reSVG.FindAllStringSubmatch(path, -1); var lastX, lastY float64
- for idx := 0; idx < len(matches); {
- item := matches[idx][0]
- if item == "M" || item == "L" {
- if idx+2 < len(matches) {
- x, _ := strconv.ParseFloat(matches[idx+1][0], 64); y, _ := strconv.ParseFloat(matches[idx+2][0], 64); lastX, lastY = x, y
- if flipY { y = 1024 - y }; pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}); idx += 3
- } else { idx++ }
- } else if item == "C" {
- if idx+6 < len(matches) {
- x1, _ := strconv.ParseFloat(matches[idx+1][0], 64); y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
- x2, _ := strconv.ParseFloat(matches[idx+3][0], 64); y2, _ := strconv.ParseFloat(matches[idx+4][0], 64)
- x, _ := strconv.ParseFloat(matches[idx+5][0], 64); y, _ := strconv.ParseFloat(matches[idx+6][0], 64)
- for t := 0.2; t <= 1.0; t += 0.2 {
- tx := math.Pow(1-t, 3)*lastX + 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)*lastY + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y
- ty_f := ty; if flipY { ty_f = 1024 - ty }; pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
+ var pts []gopdf.Point
+ matches := reSVG.FindAllStringSubmatch(path, -1)
+ var lastX, lastY float64
+ for idx := 0; idx < len(matches); {
+ item := matches[idx][0]
+ switch item {
+ case "M", "L":
+ if idx+2 < len(matches) {
+ x, _ := strconv.ParseFloat(matches[idx+1][0], 64)
+ y, _ := strconv.ParseFloat(matches[idx+2][0], 64)
+ lastX, lastY = x, y
+ if flipY {
+ y = 1024 - y
+ }
+ pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
+ idx += 3
+ } else {
+ idx++
+ }
+ case "C":
+ if idx+6 < len(matches) {
+ x1, _ := strconv.ParseFloat(matches[idx+1][0], 64)
+ y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
+ x2, _ := strconv.ParseFloat(matches[idx+3][0], 64)
+ y2, _ := strconv.ParseFloat(matches[idx+4][0], 64)
+ x, _ := strconv.ParseFloat(matches[idx+5][0], 64)
+ y, _ := strconv.ParseFloat(matches[idx+6][0], 64)
+ for t := 0.2; t <= 1.0; t += 0.2 {
+ invT := 1 - t
+ tx := invT*invT*invT*lastX + 3*invT*invT*t*x1 + 3*invT*t*t*x2 + t*t*t*x
+ ty := invT*invT*invT*lastY + 3*invT*invT*t*y1 + 3*invT*t*t*y2 + t*t*t*y
+ ty_f := ty
+ if flipY {
+ ty_f = 1024 - ty
+ }
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
+ }
+ lastX, lastY = x, y
+ idx += 7
+ } else {
+ idx++
+ }
+ case "Q":
+ if idx+4 < len(matches) {
+ x1, _ := strconv.ParseFloat(matches[idx+1][0], 64)
+ y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
+ x, _ := strconv.ParseFloat(matches[idx+3][0], 64)
+ y, _ := strconv.ParseFloat(matches[idx+4][0], 64)
+ for t := 0.2; t <= 1.0; t += 0.2 {
+ invT := 1 - t
+ tx := invT*invT*lastX + 2*invT*t*x1 + t*t*x
+ ty := invT*invT*lastY + 2*invT*t*y1 + t*t*y
+ ty_f := ty
+ if flipY {
+ ty_f = 1024 - ty
+ }
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
+ }
+ lastX, lastY = x, y
+ idx += 5
+ } else {
+ idx++
+ }
+ default:
+ idx++
}
- lastX, lastY = x, y; idx += 7
- } else { idx++ }
- } else if item == "Q" {
- if idx+4 < len(matches) {
- x1, _ := strconv.ParseFloat(matches[idx+1][0], 64); y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
- x, _ := strconv.ParseFloat(matches[idx+3][0], 64); y, _ := strconv.ParseFloat(matches[idx+4][0], 64)
- for t := 0.2; t <= 1.0; t += 0.2 {
- tx := math.Pow(1-t, 2)*lastX + 2*(1-t)*t*x1 + math.Pow(t, 2)*x
- ty := math.Pow(1-t, 2)*lastY + 2*(1-t)*t*y1 + math.Pow(t, 2)*y
- ty_f := ty; if flipY { ty_f = 1024 - ty }; pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
- }
- lastX, lastY = x, y; idx += 5
- } else { idx++ }
- } else { idx++ }
- }
+ }
+
return pts
}
diff --git a/pkg/zitie/logic/types.go b/pkg/zitie/logic/types.go
index a745f71..01ff40b 100644
--- a/pkg/zitie/logic/types.go
+++ b/pkg/zitie/logic/types.go
@@ -1,9 +1,9 @@
package logic
type HanziData struct {
- Character string `json:"character"`
- Strokes []string `json:"strokes"`
- Medians [][]Point `json:"medians"`
+ Character string `json:"character"`
+ Strokes []string `json:"strokes"`
+ Medians [][]Point `json:"medians"`
}
type Point [2]float64
diff --git a/pkg/zitie/tool.go b/pkg/zitie/tool.go
index aa535bc..f68cf6b 100644
--- a/pkg/zitie/tool.go
+++ b/pkg/zitie/tool.go
@@ -28,10 +28,12 @@ func init() {
base.Register(&zitieTool{})
}
-func (t *zitieTool) ID() string { return "zitie" }
-func (t *zitieTool) Name() string { return "汉字字帖生成" }
-func (t *zitieTool) Description() string { return "提供智能缺字处理和古风排版的专业字帖工具" }
-func (t *zitieTool) Emoji() string { return "🎨" }
+func (t *zitieTool) ID() string { return "zitie" }
+func (t *zitieTool) Name() string { return "汉字字帖生成" }
+func (t *zitieTool) Description() string {
+ return "提供智能缺字处理和古风排版的专业字帖工具"
+}
+func (t *zitieTool) Emoji() string { return "🎨" }
func (t *zitieTool) Init() error {
fmt.Println("Initializing Zitie tool with font check...")
@@ -39,21 +41,23 @@ func (t *zitieTool) Init() error {
if err := json.Unmarshal(allDataContent, &t.allChars); err != nil {
return fmt.Errorf("failed to unmarshal all.json: %v", err)
}
-
+
fontBytes, _ := fontFS.ReadFile("data/font.ttf")
logic.SetFontData(fontBytes)
fonts := map[string]string{
- "kaiti": "data/kaiti.ttf",
- "lishu": "data/lishu.ttf",
- "xingshu": "data/xingshu.ttf",
- "songti": "data/songti.ttf",
+ "kaiti": "data/kaiti.ttf",
+ "lishu": "data/lishu.ttf",
+ "xingshu": "data/xingshu.ttf",
+ "songti": "data/songti.ttf",
}
for id, src := range fonts {
bytes, err := fontFS.ReadFile(src)
- if err != nil { continue }
-
+ if err != nil {
+ continue
+ }
+
// 1. 同步到磁盘用于 gopdf
tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("own_tools_%s.ttf", id))
_ = os.WriteFile(tmpPath, bytes, 0644)
@@ -66,7 +70,7 @@ func (t *zitieTool) Init() error {
fmt.Printf("Font [%s] analyzed and registered\n", id)
}
}
-
+
return nil
}
@@ -79,7 +83,7 @@ func (t *zitieTool) RegisterRoutes(r *gin.RouterGroup) {
type ZitieRequest struct {
Chars string `json:"chars" binding:"required"`
PaperSize string `json:"paper_size"`
- FontType string `json:"font_type"`
+ FontType string `json:"font_type"`
}
func (t *zitieTool) handleTeaching(c *gin.Context) {
@@ -108,7 +112,9 @@ func (t *zitieTool) handleManuscript(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
- if req.FontType == "" { req.FontType = "kaiti" }
+ if req.FontType == "" {
+ req.FontType = "kaiti"
+ }
data := t.filterChars(req.Chars, true)
t.generateAndResponse(c, data, "manuscript", req.PaperSize, req.FontType)
}
@@ -117,18 +123,29 @@ func (t *zitieTool) filterChars(input string, keepNewline bool) []logic.HanziDat
var res []logic.HanziData
// 扩充标点符号列表
puncs := ",。!?;:、“”()《》〈〉…·.?!,:;\"'()<> 「」【】『』〔〕"
-
+
for _, r := range input {
charStr := string(r)
if r == '\n' {
- if keepNewline { res = append(res, logic.HanziData{Character: "\n"}) }
+ if keepNewline {
+ res = append(res, logic.HanziData{Character: "\n"})
+ }
continue
}
- if r == ' ' || r == '\r' || r == '\t' { continue }
-
+ if r == ' ' || r == '\r' || r == '\t' {
+ continue
+ }
+
isPunc := false
- for _, p := range puncs { if r == p { isPunc = true; break } }
- if isPunc { continue }
+ for _, p := range puncs {
+ if r == p {
+ isPunc = true
+ break
+ }
+ }
+ if isPunc {
+ continue
+ }
if hd, ok := t.allChars[charStr]; ok {
hd.Character = charStr