From 7cd611140eaa28dda1c95232dbdaf98fc4bf8161 Mon Sep 17 00:00:00 2001 From: haopengzhan Date: Wed, 25 Feb 2026 23:29:35 -0800 Subject: [PATCH] feat: enhance learn-number with dynamic SVG icons, multi-page support, and modular UI --- .gitignore | 2 +- cmd/toolbox/main.go | 15 +- cmd/toolbox/web/index.html | 106 +- cmd/toolbox/web/layout.html | 2 +- cmd/toolbox/web/tools/learn_number.html | 164 +- cmd/toolbox/web/tools/zitie.html | 80 +- pkg/base/tool.go | 18 +- pkg/learnnumber/data/fruit.svg | 3830 +++++++++++++++++++++++ pkg/learnnumber/data/icons.go | 102 +- pkg/learnnumber/logic/counting.go | 273 +- pkg/learnnumber/tool.go | 16 +- pkg/zitie/tool.go | 1 + 12 files changed, 4330 insertions(+), 279 deletions(-) create mode 100644 pkg/learnnumber/data/fruit.svg diff --git a/.gitignore b/.gitignore index 36e8abd..e4b25c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Binaries -/toolbox +toolbox *.exe *.exe~ *.dll diff --git a/cmd/toolbox/main.go b/cmd/toolbox/main.go index e09e607..ee08f33 100644 --- a/cmd/toolbox/main.go +++ b/cmd/toolbox/main.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "os" + "sort" "strconv" "toolbox/pkg/base" _ "toolbox/pkg/learnnumber" @@ -81,11 +82,17 @@ func main() { r.GET("/", serveIndex) r.GET("/api/tools", func(c *gin.Context) { var list []map[string]string - for _, t := range base.Registry { + ids := make([]string, 0, len(base.Registry)) + for id := range base.Registry { ids = append(ids, id) } + sort.Strings(ids) + + for _, id := range ids { + t := base.Registry[id] list = append(list, map[string]string{ - "id": t.ID(), - "name": t.Name(), - "desc": t.Description(), + "id": t.ID(), + "name": t.Name(), + "desc": t.Description(), + "emoji": t.Emoji(), }) } c.JSON(http.StatusOK, list) diff --git a/cmd/toolbox/web/index.html b/cmd/toolbox/web/index.html index 6917f4c..7bdcf25 100644 --- a/cmd/toolbox/web/index.html +++ b/cmd/toolbox/web/index.html @@ -1,80 +1,48 @@ {{define "content"}} - - -{{template "zitie" .}} -{{template "learn_number" .}} - - -
+ +
- -
-
-

🖋️ 汉字字帖生成

-

支持多种书法字体的 2x3 教学方格、步进分解和古风竖排信纸。

-
立即开始 →
-
-
-

🔢 幼儿数学助手

-

数图形、基础算术等趣味练习。培养孩子的数感与逻辑。

-
立即开始 →
-
+ +
+
+ +{{template "zitie" .}} +{{template "learn_number" .}} + {{end}} diff --git a/cmd/toolbox/web/layout.html b/cmd/toolbox/web/layout.html index 3660021..9eb3c4d 100644 --- a/cmd/toolbox/web/layout.html +++ b/cmd/toolbox/web/layout.html @@ -113,7 +113,7 @@ tools.forEach(tool => { const item = document.createElement('div'); item.className = 'nav-item'; item.id = `nav-${tool.id}`; - item.innerHTML = `🛠️ ${tool.name}`; + item.innerHTML = `${tool.emoji || '🛠️'} ${tool.name}`; item.onclick = () => { navigateTo(`/${tool.id}`); document.getElementById('menu-toggle').checked = false; }; navList.appendChild(item); }); diff --git a/cmd/toolbox/web/tools/learn_number.html b/cmd/toolbox/web/tools/learn_number.html index 780d066..f61026a 100644 --- a/cmd/toolbox/web/tools/learn_number.html +++ b/cmd/toolbox/web/tools/learn_number.html @@ -2,37 +2,161 @@
数图形练习
-
-
-
- - - 3 -
-
- - - 15 -
-
- - + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ + -
- + + + + {{end}} diff --git a/cmd/toolbox/web/tools/zitie.html b/cmd/toolbox/web/tools/zitie.html index 6fc07f5..1da5523 100644 --- a/cmd/toolbox/web/tools/zitie.html +++ b/cmd/toolbox/web/tools/zitie.html @@ -1,43 +1,77 @@ {{define "zitie"}}
-
2x3 教学方格
-
步进式分解
-
古风竖排
+
教学版 (2x3)
+
临摹版 (9列)
+
古风纵写
-
-
- - -
-
+
+ +
- - + -
- - +
- + + {{end}} diff --git a/pkg/base/tool.go b/pkg/base/tool.go index e3296c0..8f177ed 100644 --- a/pkg/base/tool.go +++ b/pkg/base/tool.go @@ -1,22 +1,18 @@ package base -import ( - "github.com/gin-gonic/gin" -) +import "github.com/gin-gonic/gin" -// Tool 定义了工具箱中每个子工具必须实现的接口 type Tool interface { - ID() string // 工具的唯一标识,用于路由前缀,如 "zitie" - Name() string // 工具的显示名称 - Description() string // 工具的描述 - Init() error // 初始化逻辑,如加载 embed 的数据 - RegisterRoutes(r *gin.RouterGroup) // 注册该工具的 API 路由 + ID() string + Name() string + Description() string + Emoji() string + Init() error + RegisterRoutes(r *gin.RouterGroup) } -// Registry 存储所有已注册的工具 var Registry = make(map[string]Tool) -// Register 用于工具在 init() 函数中注册自己 func Register(t Tool) { Registry[t.ID()] = t } diff --git a/pkg/learnnumber/data/fruit.svg b/pkg/learnnumber/data/fruit.svg new file mode 100644 index 0000000..202ffdf --- /dev/null +++ b/pkg/learnnumber/data/fruit.svg @@ -0,0 +1,3830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/learnnumber/data/icons.go b/pkg/learnnumber/data/icons.go index 3f174aa..4f4c39c 100644 --- a/pkg/learnnumber/data/icons.go +++ b/pkg/learnnumber/data/icons.go @@ -1,41 +1,77 @@ package data +import ( + "encoding/xml" + "fmt" + "os" + "strings" +) + type Icon struct { Name string Paths []string } -// 升级版:更卡通、更饱满的简笔画 -var CountingIcons = []Icon{ - {Name: "Bear", Paths: []string{ - "M 512 800 C 300 800 200 700 200 500 C 200 300 350 200 512 200 C 674 200 824 300 824 500 C 824 700 724 800 512 800 Z", // Body - "M 300 300 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Ear L - "M 724 300 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Ear R - "M 400 450 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye L - "M 624 450 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye R - "M 512 550 Q 512 650 400 650 M 512 550 Q 512 650 624 650", // Mouth - }}, - {Name: "Cat", Paths: []string{ - "M 200 800 L 300 400 L 400 200 L 512 350 L 624 200 L 724 400 L 824 800 Z", // Head - "M 400 550 m -15 0 a 15 15 0 1 0 30 0 a 15 15 0 1 0 -30 0", // Eye L - "M 624 550 m -15 0 a 15 15 0 1 0 30 0 a 15 15 0 1 0 -30 0", // Eye R - "M 512 650 L 450 700 M 512 650 L 574 700", // Nose - }}, - {Name: "Car", Paths: []string{ - "M 100 700 L 100 500 Q 100 400 300 400 L 700 400 Q 900 400 900 500 L 900 700 Z", // Body - "M 250 700 m -60 0 a 60 60 0 1 0 120 0 a 60 60 0 1 0 -120 0", // Wheel L - "M 750 700 m -60 0 a 60 60 0 1 0 120 0 a 60 60 0 1 0 -120 0", // Wheel R - "M 300 400 L 400 250 L 624 250 L 724 400", // Roof - }}, - {Name: "Bird", Paths: []string{ - "M 512 512 m -300 0 a 300 300 0 1 0 600 0 a 300 300 0 1 0 -600 0", // Body - "M 812 512 L 950 450 L 812 400 Z", // Beak - "M 400 400 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye - "M 212 512 Q 100 400 212 300", // Wing - }}, - {Name: "Rocket", Paths: []string{ - "M 512 100 Q 700 400 700 800 L 324 800 Q 324 400 512 100 Z", // Body - "M 512 400 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Window - "M 324 800 L 200 950 L 324 900 M 700 800 L 824 950 L 700 900", // Fins - }}, +var IconCategories = map[string][]Icon{ + "shapes": { + {Name: "Circle", Paths: []string{"M 512 112 C 291 112 112 291 112 512 C 112 733 291 912 512 912 C 733 912 912 733 912 512 C 912 291 733 112 512 112 Z"}}, + {Name: "Square", Paths: []string{"M 150 150 L 874 150 L 874 874 L 150 874 Z"}}, + {Name: "Rectangle", Paths: []string{"M 100 300 L 924 300 L 924 724 L 100 724 Z"}}, + {Name: "Triangle", Paths: []string{"M 512 150 L 900 850 L 124 850 Z"}}, + {Name: "Star", Paths: []string{"M 512 100 L 612 400 L 924 400 L 674 600 L 774 900 L 512 700 L 250 900 L 350 600 L 100 400 L 412 400 Z"}}, + {Name: "Heart", Paths: []string{"M 512 900 C 200 700 100 500 100 300 C 100 100 400 100 512 250 C 624 100 924 100 924 300 C 924 500 824 700 512 900 Z"}}, + {Name: "Diamond", Paths: []string{"M 512 100 L 900 512 L 512 924 L 124 512 Z"}}, + {Name: "Oval", Paths: []string{"M 512 350 C 200 350 100 420 100 512 C 100 604 200 674 512 674 C 824 674 924 604 924 512 C 924 420 824 350 512 350 Z"}}, + {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": {}, +} + +type SVG struct { + Groups []G `xml:"g"` +} + +type G struct { + ID string `xml:"id,attr"` + Groups []G `xml:"g"` + Paths []Path `xml:"path"` +} + +type Path struct { + D string `xml:"d,attr"` +} + +func LoadIcons(filePath string) error { + data, err := os.ReadFile(filePath) + if err != nil { return err } + var svg SVG + if err := xml.Unmarshal(data, &svg); err != nil { return err } + var objectsG *G + for i := range svg.Groups { + if svg.Groups[i].ID == "objects" { objectsG = &svg.Groups[i]; break } + } + if objectsG == nil { return nil } + var fruitIcons []Icon + for i, g := range objectsG.Groups { + paths := collectPaths(g) + if len(paths) > 0 { + fruitIcons = append(fruitIcons, Icon{ + Name: fmt.Sprintf("Item %d", i+1), + Paths: paths, + }) + } + } + IconCategories["fruits"] = fruitIcons + return nil +} + +func collectPaths(g G) []string { + var paths []string + for _, p := range g.Paths { + d := strings.TrimSpace(p.D) + if d != "" { paths = append(paths, d) } + } + for _, subG := range g.Groups { paths = append(paths, collectPaths(subG)...) } + return paths } diff --git a/pkg/learnnumber/logic/counting.go b/pkg/learnnumber/logic/counting.go index 5a4b683..02bcc8e 100644 --- a/pkg/learnnumber/logic/counting.go +++ b/pkg/learnnumber/logic/counting.go @@ -16,10 +16,21 @@ import ( type CountingRequest struct { TotalCount int `json:"total_count"` IconTypes int `json:"icon_types"` + PageCount int `json:"page_count"` + Category string `json:"category"` PaperSize string `json:"paper_size"` } -var reSVG = regexp.MustCompile(`([MLQCZmlqcz])|(-?\d+\.?\d*)`) +type PlacedIcon struct { + X, Y, Radius float64 +} + +type PathResult struct { + Points []gopdf.Point + Closed bool +} + +var reSVGToken = regexp.MustCompile(`[a-zA-Z]|-?\d+\.?\d*`) func GenerateCountingPDF(req CountingRequest) ([]byte, error) { rand.Seed(time.Now().UnixNano()) @@ -27,142 +38,182 @@ func GenerateCountingPDF(req CountingRequest) ([]byte, error) { 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) + // 硬性限制:页数 1-10 页,防止资源耗尽 + if req.PageCount < 1 { req.PageCount = 1 } + if req.PageCount > 10 { req.PageCount = 10 } + + for i := 0; i < req.PageCount; i++ { + pdf.AddPage() + drawProblemV4(pdf, req, 0, rect.W, rect.H/2) + drawProblemV4(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) { +func drawProblemV4(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 + boxW, boxH := (pW-2*margin)*0.7, pH-60.0 + xBase, yBase := margin, startY + 30.0 - // 1. 绘制方框 - pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.0) - pdf.RectFromUpperLeft(x, y, boxW, boxH) + pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.5) + pdf.RectFromUpperLeft(xBase, yBase, boxW, boxH) - // 2. 分配图标数量 - numTypes := req.IconTypes; if numTypes < 1 { numTypes = 1 } - if numTypes > len(data.CountingIcons) { numTypes = len(data.CountingIcons) } + catIcons := data.IconCategories[req.Category] + if len(catIcons) == 0 { catIcons = data.IconCategories["shapes"] } - total := req.TotalCount; if total < numTypes { total = numTypes } - if total > 30 { total = 30 } - - // 随机选出图标种类 - allIcons := rand.Perm(len(data.CountingIcons)) - selectedIcons := []data.Icon{} - counts := []int{} + numTypes := req.IconTypes + if numTypes < 1 { numTypes = 1 }; if numTypes > 6 { numTypes = 6 } + if numTypes > len(catIcons) { numTypes = len(catIcons) } - // 先每种分配1个 - rem := total - numTypes + total := req.TotalCount + 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, data.CountingIcons[allIcons[i]]) - counts = append(counts, 1) + selectedIcons = append(selectedIcons, catIcons[allIconsPerm[i]]) + counts[i] = 1 } - // 随机分配剩下的 - for i := 0; i < rem; i++ { + remaining := total - numTypes + for i := 0; i < remaining; 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 + avgRadius := math.Sqrt((boxW * boxH * 0.22) / (float64(total) * math.Pi)) + if avgRadius > 35.0 { avgRadius = 35.0 } - iconTypeIdx := 0 - countInCurrentType := 0 - + var placed []PlacedIcon + iconTypeIdx, currentInType := 0, 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) + 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 } } + if !collision { + drawSimpleIcon(pdf, selectedIcons[iconTypeIdx], randX, randY, r*2) + placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r}); break + } + } + 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) + } +} + +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 + 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 } + } + } + curW, curH := maxX-minX, maxY-minY + 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 + + pdf.SetStrokeColor(0, 0, 0) + if strings.Contains(strings.ToLower(icon.Name), "item") || len(icon.Paths) > 1 { + pdf.SetLineWidth(size / 60.0) // 复杂素材用细线 (30% less than 45 is approx 60) + } else { + pdf.SetLineWidth(size / 22.0) // 基础图形用粗线 + } + 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} } + 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) } + } } } } -// 支持贝塞尔曲线采样,确保图标圆润 -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}) +func parseMultiPath(icon data.Icon, scale, ox, oy float64) []PathResult { + var results []PathResult + for _, path := range icon.Paths { + tokens := reSVGToken.FindAllString(path, -1) + var lx, ly, startX, startY float64 + var pts []gopdf.Point + var currentCmd string + var isRel bool + for i := 0; i < len(tokens); { + token := tokens[i] + 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 } + } else if cmd == "Z" { + 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++ + continue } - 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}) + 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 } + lx, ly = x, y + 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++ + 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++ + 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}) + } + 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}) + } + lx, ly = x, y; i += 4 + case "A": i += 7 + default: i++ } - lx, ly = x, y; i += 5 - } else { i++ } + } + if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}) } } - return pts + return results } diff --git a/pkg/learnnumber/tool.go b/pkg/learnnumber/tool.go index 25b70ea..1223137 100644 --- a/pkg/learnnumber/tool.go +++ b/pkg/learnnumber/tool.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "toolbox/pkg/base" + "toolbox/pkg/learnnumber/data" "toolbox/pkg/learnnumber/logic" "github.com/gin-gonic/gin" @@ -18,14 +19,22 @@ func init() { 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...") - return nil + // 动态加载匿名对象 + return data.LoadIcons("pkg/learnnumber/data/fruit.svg") } func (t *learnNumberTool) RegisterRoutes(r *gin.RouterGroup) { r.POST("/counting", t.handleCounting) + r.GET("/categories", t.handleCategories) +} + +func (t *learnNumberTool) handleCategories(c *gin.Context) { + // 返回分类信息供前端预览 + c.JSON(http.StatusOK, data.IconCategories) } func (t *learnNumberTool) handleCounting(c *gin.Context) { @@ -34,15 +43,10 @@ func (t *learnNumberTool) handleCounting(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - - if req.TotalCount <= 0 { req.TotalCount = 20 } - if req.TotalCount > 30 { req.TotalCount = 30 } - pdfBytes, err := logic.GenerateCountingPDF(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.Data(http.StatusOK, "application/pdf", pdfBytes) } diff --git a/pkg/zitie/tool.go b/pkg/zitie/tool.go index b22d45e..aa535bc 100644 --- a/pkg/zitie/tool.go +++ b/pkg/zitie/tool.go @@ -31,6 +31,7 @@ func init() { 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...")