# AOTLogoSharp **Repository Path**: JTsamsde/AOTLogoSharp ## Basic Information - **Project Name**: AOTLogoSharp - **Description**: LogoSharp的AOT兼容版本 - **Primary Language**: C# - **License**: BSD-3-Clause - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-10 - **Last Updated**: 2026-07-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AOTLogoSharp 面向托管世界的 Logo 编程语言。 [English document](https://4darmygeometry.github.io/AOTLogoSharp/) [![Build status](https://img.shields.io/github/actions/workflow/status/4Darmygeometry/AOTLogoSharp/build.yml?label=build&logo=github)](https://github.com/4Darmygeometry/AOTLogoSharp/actions/workflows/build.yml) [![NuGet Badge](https://img.shields.io/nuget/v/AOTLogoSharp.svg?logo=nuget)](https://www.nuget.org/packages/AOTLogoSharp/) ## AOTLogoSharp 是什么? AOTLogoSharp 是 Logo 编程语言的 .NET 实现,基于手写递归下降解析器。 ## 为什么选择 AOTLogoSharp? AOTLogoSharp 完全兼容 **Native AOT** 和 **裁剪(Trimming)**。你可以将应用程序发布为体积小巧的单文件原生可执行文件,启动速度快,无 JIT 开销。这非常适合对部署体积和启动速度有要求的场景,例如命令行工具、教育应用或嵌入式绘图流水线。 程序集名称保持为 `LogoSharp.dll` 和 `LogoSharp.Drawing.dll`,命名空间仍为 `LogoSharp` / `LogoSharp.Drawing`,因此引用原版 LogoSharp 的现有代码只需更换 NuGet 包即可迁移。 ## 如何使用 AOTLogoSharp? 在基于 .NET Framework 4.8、.NET Standard 2.0、.NET Standard 2.1 或 .NET 10 的 .NET 项目中使用 AOTLogoSharp 非常简单直接。这意味着你可以让 AOTLogoSharp 应用与 .NET Core 一起工作,从而具备跨平台能力。 1. 安装 AOTLogoSharp NuGet 包,例如: ``` Install-Package AOTLogoSharp -Version 1.9.2 ``` 2. 编写你的第一个应用: ```csharp using LogoSharp; static void Main(string[] args) { var logo = new Logo(); logo.Forward += (s, e) => Console.WriteLine($"Forwarded {e.Steps} steps."); logo.Execute("FD 102"); } ``` 3. 运行你的应用,你应该会在控制台看到消息 `Forwarded 102 steps.` 基本上,Logo 程序代码通过 `logo` 实例的 `Execute` 方法传入,AOTLogoSharp 会执行代码并触发事件。因此,订阅了特定事件的事件处理程序会在代码执行触发该订阅事件时被调用。 `Logo` 类提供 `HeadingMode` 属性来控制 `SETH` / `SETHEADING` 的角度语义: - `"logo"`(默认):0° = 正北,顺时针增加。符合标准 Logo 语言规范。 - `"standard"`:0° = 正东,逆时针增加。符合数学极坐标惯例。 ```csharp var logo = new Logo(); logo.HeadingMode = "standard"; // 切换到数学角度语义 logo.Execute("SETH 0"); // 此时指向正东而非正北 ``` `Logo` 类还提供 `WaitMode` 属性来控制 `WAIT` 命令的延时单位: - `"logo"`(默认):1 刻度 = 1/60 秒(约 16.67 毫秒),遵循传统 Logo 规范。 - `"ms"` / `"standard"`:1 刻度 = 1 毫秒。 ```csharp var logo = new Logo(); logo.WaitMode = "logo"; // WAIT 1 ≈ 16.67 ms logo.Execute("WAIT 60"); // 等待约 1 秒 ``` ### PRINT 命令与 `Print` 事件 AOTLogoSharp 支持 `PRINT` 命令,提供多种形式: - `PRINT "text` — 输出字符串字面量(`"` 之后到下一个空白 / 分隔符之间的全部内容)。 - `PRINT 42` — 输出数值。 - `PRINT [word1 word2 ...]` — 输出列表字面量,空格拼接为一个句子。 - `PRINT (expr)` — 输出表达式结果(当 `(...)` 内**没有** `[...]` 时)。 - `PRINT ([文本] :var)` — **混合拼接**:列表 + 变量(空格分隔)。 - `PRINT ([文本] + :var)` — **混合拼接**:列表 + 变量(`+` 号表示无空格拼接)。 - `PRINT (:var [文本] :var)` — **混合拼接**:变量 + 列表 + 变量(空格分隔)。 - `PRINT (:var + [文本] + :var)` — **混合拼接**:变量 + 列表 + 变量(无空格拼接)。 - `PRINT ({expr} + [文本] + :var)` — **混合拼接**:计算表达式 + 列表 + 变量。 - `PRINT (SE 1 2 3)` — **增强语法**:多参 SE/WORD/LIST 函数调用。 **混合拼接模式**在 `(...)` 内出现**任意** `[...]` 列表字面量时自动激活。在此模式下: - `+` 作为**无空格连接符**(不是算术加法)。 - 没有 `+` 的相邻元素之间插入一个空格。 - `[...]` 内的内容视为纯文本。源代码中没有空格间隔的相邻字符会保持合并(如 `[π/]` → `π/`,而非 `π /`)。 - `{expr}` 可用于**计算值**(如 `{:x + 2}` 先计算加法,再参与拼接)。 - 当 `(...)` 内**没有** `[...]` 时,`(...)` 仍为普通算术表达式:`PRINT (1 + 2)` 输出 `3`。 示例: ```Logo MAKE "x 3 MAKE "y 2 PRINT ([The answer is] :x) ; The answer is 3 PRINT ([Answer:] + :x) ; Answer:3 PRINT (:x [π /] :y) ; 3 π / 2 PRINT (:x + [π/] + :y) ; 3π/2 PRINT ({:x + 2} + [π/] + :y) ; 5π/2 PRINT (:x + [π/] + {:y + 5}) ; 3π/7 PRINT (:x + [π/] + {:y + 5} + [≈] + {FORM :x * PI / (:y + 5) 6 4}) ; 3π/7≈1.3464 PRINT ([COS 45=] + {COS 45}) ; COS 45=0.707106781186548 PRINT (SE 1 2 3) ; 1 2 3 PRINT (WORD "A "B "C) ; ABC PRINT (LIST "A "B "C) ; A B C ``` 每次 `PRINT` 调用都会触发 `Print` 事件,类型为 `EventHandler`,渲染好的文本通过 `Text` 属性给出。字符串字面量内容**保留用户输入的原始大小写**——例如 `PRINT "Hello` 会原样输出 `Hello`,而关键字(如 `PRINT` 本身)的匹配仍然是大小写不敏感的。 ```csharp using LogoSharp; var logo = new Logo(); logo.Print += (s, e) => Console.WriteLine($"[print] {e.Text}"); logo.Execute(@" PRINT [Test OK] PRINT ""你好 世界 PRINT 3.14 "); ``` ### SE / WORD / LIST AOTLogoSharp 支持三种字符串拼接函数,可通过花括号表达式和 `(SE ...)` / `(WORD ...)` / `(LIST ...)` 调用语法访问: - **SE** *参数1 参数2 ...* — 空格拼接参数。`(SE 1 2 3)` → `1 2 3`,`(SE "A "B "C)` → `A B C`。别名:**SENTENCE**。 - **WORD** *参数1 参数2 ...* — 无空格拼接参数。`(WORD "A "B "C)` → `ABC`,`(WORD 1 2 3)` → `123`。 - **LIST** *参数1 参数2 ...* — 等价于 SE(空格拼接),兼容 FMSLogo。 可以在 `PRINT` / `LABEL` 的括号内使用:`PRINT (SE 1 2 3)` 输出 `1 2 3`。也可作为独立花括号函数:`MAKE "s {WORD "A "B "C}`。字符串参数需要前导 `"`(如 `"A`);数字参数不需要(如 `1`)。 ### LABEL 命令 `LABEL` 在海龟当前位置将文本直接绘制到画布上。别名:**TURTLETEXT**、**TT**。支持与 PRINT 相同的表达式语法: - `LABEL "text` — 单词语字符串字面量。 - `LABEL :var` — 变量值。 - `LABEL [词1 词2 ...]` — 列表字面量,空格分隔。 - `LABEL (expr)` — 表达式结果(当 `(...)` 内没有 `[...]` 时)。 - `LABEL ([文本] + :var)` — 混合拼接(规则同 PRINT)。 - `LABEL (:var [文本] :var)` — 变量 + 列表 + 变量(空格分隔)。 - `LABEL (:var + [文本] + :var)` — 变量 + 列表 + 变量(无空格拼接)。 - `LABEL ({expr} + [文本] + :var)` — 计算表达式 + 列表 + 变量。 同样适用**混合拼接模式**规则:`(...)` 内出现任何 `[...]` 时,`+` 变为无空格连接符,`{expr}` 先计算后拼接,无 `+` 的相邻元素空格分隔。 **LABEL 文本始终从左到右渲染**在画布上,不受海龟朝向影响。这与 MSWLogo 中标签文字跟随海龟朝向旋转的行为不同。 **LABEL 颜色随 SETPENCOLOR 同步**:`DrawLabel` 事件在触发时会把当前 `SETPENCOLOR` / `SETPC` 设置的 R/G/B 一并传给宿主端,宿主把 R/G/B 持久化在 `LabelRecord` 中。无头模式 (`Save()` PNG 导出) 和 Avalonia GUI (`TurtleCanvas` 的 `LabelsDrawOperation`) 都按每条 LABEL 各自的颜色画字,而不是写死黑色。这意味着在 `SETPENCOLOR [255 0 0]` 之后执行 `LABEL [红字]` 会得到红字,再 `SETPENCOLOR [0 255 0]` 之后 `LABEL [绿字]` 仍是红字。每条 LABEL 绑定执行瞬间的笔色,互相独立。 **Unicode 字体逐字符回退**:LABEL(及无头模式 `Save()` PNG 导出)使用逐字符字体回退机制。默认主字体覆盖 CJK 字符(Windows 上微软雅黑、macOS/iOS 上苹方、Android/Linux 上 Noto Sans CJK SC)。通过 `SETFONT` 指定自定义字体时,该字体作为主字体;对于主字体不包含的字符(如 `≈`、`∞`、`∑`、emoji),`SKFontManager.MatchCharacter` 会自动查找系统中包含该字形的字体作为回退。这确保混合脚本文本(如 `3π/7≈1.3464`)、中文文本(如 `中文测试`)、emoji(如 `😊`)以及 SETFONT 指定字体与 emoji 混排都能在画布上正确渲染。 示例: ```Logo MAKE "x 3 MAKE "y 2 PU LABEL ([The answer is] :x) ; The answer is 3 BK 15 LABEL ([Answer:] + :x) ; Answer:3 BK 15 LABEL (:x [π /] :y) ; 3 π / 2 BK 15 LABEL (:x + [π/] + :y) ; 3π/2 BK 15 LABEL ({:x + 2} + [π/] + :y) ; 5π/2 BK 15 LABEL (:x + [π/] + {:y + 5}) ; 3π/7 BK 15 LABEL (:x + [π/] + {:y + 5} + [≈] + {FORM :x * PI / (:y + 5) 6 4}) ; 3π/7≈1.3464 BK 15 LABEL ([COS 45=] + {COS 45}) ; COS 45=0.707106781186548 BK 15 LABEL (SE 1 2 3) ; 1 2 3 BK 15 LABEL (WORD "A "B "C) ; ABC BK 15 LABEL (LIST "A "B "C) ; A B C HOME PD ``` ### SETFONT 命令 `SETFONT` 设置后续 `LABEL` 命令使用的字体。语法: ``` SETFONT "字体名 字号 属性 SETFONT [字体名] 字号 属性 ``` - **字体名**:带引号的字面量(双引号 `"` 或方括号 `[]` 均可),如 `"宋体`、`"Arial`、`[Times New Roman]` - **多词字体名(含空格)必须用方括号**:`"Microsoft YaHei` 只会取到 `Microsoft`,必须写 `[Microsoft YaHei]` - **空方括号 `[]` 表示重置为默认 CJK 字体**:`SETFONT [] 0 0` 恢复默认 14px CJK 字体 - **字号**:正实数,指定字体的像素大小(0 表示使用默认 14px) - **属性**:位掩码整数(PCLogo 兼容),可组合相加: | 值 | 属性 | |---|---| | 1 | 粗体 (Bold) | | 2 | 斜体 (Italic) | | 4 | 下划线 (Underline) | | 8 | 删除线 (Strikeout) | 示例: ```Logo SETFONT "宋体 24 0 ; 24 号常规宋体 LABEL [中文标题] SETFONT "Arial 18 1 ; 18 号粗体 Arial LABEL [Bold Title] SETFONT [Courier New] 16 2 ; 16 号斜体 Courier New LABEL [Italic Monospace] SETFONT [Times New Roman] 20 3 ; 20 号粗斜体 Times New Roman LABEL [Bold Italic Serif] SETFONT "Arial 22 4 ; 22 号下划线 Arial LABEL [Underlined Text] SETFONT "Arial 20 12 ; 20 号下划线+删除线 (4+8) LABEL [Underline+Strike] SETFONT [] 0 0 ; 重置为默认 CJK 字体 LABEL [恢复默认] ``` **字体回退**:如果指定的字体在当前系统上不可用,自动回退到内置 CJK 字体(Windows: 微软雅黑、macOS: 苹方、Linux: Noto Sans CJK SC)。宋体类字体回退宋体类,黑体类字体回退黑体类。 `SETFONT` 触发 `SetFont` 事件(`EventHandler`),包含 `FontName`、`FontSize`、`Style` 三个属性。宿主端调用 `turtle.SetFont(fontName, fontSize, style)` 落地。 **LABEL 默认字号**:未调用 SETFONT 时,LABEL 使用默认字号 14px(随画布 scale 缩放)。 **清屏重置字体**:执行 `CLEARSCREEN` / `CS` / `CLS` / `DRAW` / `CLEARSCR` 后,SETFONT 设置的字体状态会恢复为默认值(默认 CJK 字体、14px、常规样式),与画笔默认状态一并复位。`HOME` 命令不改变字体设置。 ### FORM 命令 `FORM` *数字 总宽度 小数位数* 是一个三参数格式化函数,生成右对齐固定宽度字符串(FMSLogo 兼容)。若格式化后的数字短于请求宽度,前方补空格。 - `PRINT FORM 3.14159265 6 2` → ` 3.14` - `LABEL FORM :x 4 1` → 在画布上绘制如 ` 1.2` 或 `-0.6`。 - `{FORM 1.5 4 2}` → `1.50`(独立花括号表达式)。 - 复数:`PRINT FORM 3 + 4i 16 2` → ` 3.00+4.00i`(精度同时影响实部和虚部,总宽度为复数字符串总长 + 补空格数)。 FORM 不是变参函数——始终要求恰好三个参数。第一个参数**必须是数值**(字面量、变量或表达式);传入字符串(如 `FORM "中文测试 6 2` 或 `FORM 中文测试 6 2`)是不合法的。既支持 `PRINT FORM ...` / `LABEL FORM ...`(解析器级简写),也支持 `{FORM ...}`(通用花括号表达式)。 ### 错误报告 运行时错误(例如调用未定义的过程)会带上精确的 `[行:列]` 源码位置信息。当一次 `Execute` 调用中出现多个错误时,引擎会把它们聚合到同一个 `RuntimeException` 中,`Message` 里每行一条错误,便于宿主 UI 把每条错误作为独立列表项展示。 ### STOP 命令与递归 / 循环上限 `STOP` 是内置关键字,立即退出当前正在执行的最内层过程。它**不会**触发任何事件——而是在引擎内部抛出一个 `StopException`,由过程调度器吞掉,宿主 UI 不会看到崩溃。在过程外使用 `STOP` 没有效果(异常找不到捕获边界,会被重新抛出为普通的 `RuntimeException`)。 引擎为失控的 Logo 程序提供两道语言级防线,挂在 `Logo` 实例的两个公开属性上: - `MaxRepeatIterations`(默认 `10000`)— 同时限制单条 `REPEAT` 的次数和 `WHILE` 循环的每步迭代次数。也适用于 `FOR`:执行前会根据起始值、终止值和步长预计算迭代次数,超过此限制则拒绝执行。嵌套 `FOR` 的每一层独立受限,因此嵌套循环的实际总工作量上限为 `MaxRepeatIterations^嵌套层数`。对于高次数非嵌套循环,建议使用 `REPEAT` 而非 `FOR`,因为 `REPEAT` 更简单且无控制变量开销。超过后会抛 `RuntimeException`,`Message` 以 `StackOverflowException` 前缀开头。 - `MaxCallStackDepth`(默认 `500`)— 限制用户自定义过程调用的递归深度。 ```csharp var logo = new Logo { MaxRepeatIterations = 1000000, // 允许长时间运行的绘图 MaxCallStackDepth = 2000 // 允许更深的递归 }; logo.Execute("REPEAT 1000000 [FD 1]"); ``` 对于语言级限制无法覆盖的极端情况(病态输入导致 AST 无界增长等),还提供进程级看门狗 `LogoSharp.MemoryGuard`:周期性采样当前进程的私有内存,越界后立即 `Environment.Exit(1)` 退出: ```csharp using LogoSharp; // 启动 1 GiB 看门狗(默认:1024 MB 阈值,1000 ms 检测间隔) MemoryGuard.Start(thresholdMB: 1024, checkIntervalMs: 1000); // ... 执行 Logo 代码 ... MemoryGuard.Stop(); ``` 有关如何使用 AOTLogoSharp 的更多信息,请参阅 `LogoSharp.Drawing` 项目。 ### 使用 AOTLogoSharp.Drawing `AOTLogoSharp.Drawing` 提供了 `Turtle` 类,用于将 Logo 程序渲染为图像。它也可以作为 `AOTLogoSharp.Drawing` NuGet 包使用。 1. 安装 AOTLogoSharp.Drawing NuGet 包,例如: ``` Install-Package AOTLogoSharp.Drawing -Version 1.9.2 ``` 2. 编写你的第一个应用: ```csharp using LogoSharp; using LogoSharp.Drawing; using SkiaSharp; static void Main(string[] args) { var turtle = new Turtle(); var logo = new Logo(); // 将 Logo 事件连接到 Turtle logo.TurnLeft += (s, e) => turtle.Left(e.Angle); logo.TurnRight += (s, e) => turtle.Right(e.Angle); logo.SetHeading += (s, e) => turtle.Angle = e.Angle; logo.Forward += (s, e) => turtle.MoveForward(e.Steps); logo.Backward += (s, e) => turtle.MoveBackward(e.Steps); logo.PenUp += (s, e) => turtle.PenStatus = PenStatus.Up; logo.PenDown += (s, e) => turtle.PenStatus = PenStatus.Down; logo.SetPenColor += (s, e) => { turtle.SetPenColor(e.R, e.G, e.B); var color = Color.FromRgb( (byte)Math.Clamp(e.R, 0, 255), (byte)Math.Clamp(e.G, 0, 255), (byte)Math.Clamp(e.B, 0, 255)); // 关键:UI 操作必须在 UI 线程执行 Dispatcher.UIThread.Invoke(() => { PenColor = new SolidColorBrush(color); StrokeColor = color; }); }; logo.SetPenSize += (s, e) => turtle.SetPenWidth(e.Width); logo.Delay += (s, e) => turtle.DelayMilliseconds = e.Milliseconds; logo.Wait += (s, e) => { turtle.WaitMode = logo.WaitMode; turtle.WaitOneShot(e.Ticks); }; logo.ClearScreen += (s, e) => turtle.Clear(); logo.GoHome += (s, e) => turtle.Reset(); logo.ShowTurtle += (s, e) => turtle.ShowTurtle(); logo.HideTurtle += (s, e) => turtle.HideTurtle(); logo.PenErase += (s, e) => turtle.PenErase(); logo.PenNormal += (s, e) => turtle.PenNormal(); logo.SetX += (s, e) => turtle.SetX(e.X); logo.SetY += (s, e) => turtle.SetY(e.Y); logo.SetXY += (s, e) => turtle.SetXY(e.X, e.Y); // 新增:LABEL 文本绘制事件 logo.DrawLabel += (s, e) => turtle.DrawLabel(e.Text, e.X, e.Y, e.FontName, e.FontSize, e.FontStyle, e.R, e.G, e.B); // 新增:SETFONT 字体设置事件 logo.SetFont += (s, e) => turtle.SetFont(e.FontName, e.FontSize, e.Style); logo.Execute(@" REPEAT 4 [ FD 100 RT 90 ] "); // 改用统一渲染方法 RenderToCanvas(替代直接 turtle.Save) var info = new SKImageInfo(800, 600); using var surface = SKSurface.Create(info); turtle.RenderToCanvas(surface.Canvas, 800, 600, 1.0f, drawTurtleIcon: true); using var image = surface.Snapshot(); using var data = image.Encode(SKEncodedImageFormat.Png, 100); using var stream = File.OpenWrite("output.png"); data.SaveTo(stream); } ``` `Turtle` 类公开了以下 API: - `SetCanvasSize(int width, int height)` — 设置画布大小 - `ShowTurtle()` / `HideTurtle()` — 切换海龟可见性 - `Save(string fileName)` — 将渲染的图像保存为文件 - `GetLineSegments()` — 获取绘制的线段列表,用于自定义渲染 - `GetTurtleState()` — 获取当前海龟的位置、角度和可见性 - **`RenderToCanvas(SKCanvas canvas, float canvasWidth, float canvasHeight, float scale, bool drawTurtleIcon)`** — 统一渲染方法,将所有绘图元素(线段、圆点、圆弧、LABEL、海龟图标)绘制到任意 `SKCanvas` 上。无头模式 PNG 导出(`CreateRenderSurface`)和 Avalonia GUI 控件(`TurtleCanvas`)均复用此方法,避免渲染逻辑重复。`scale` 参数控制世界坐标→屏幕像素的缩放比(1.0 = 1:1)。参见 `TurtleCanvas.cs` 了解 GUI 集成示例。 - `DelayMilliseconds` — 控制绘图步骤之间的延迟。**该延时仅作用于画线操作**(PENDOWN / PENERASE)。非画线移动(如抬笔状态下的 `SetX` / `SetY` / `SetXY` / `MoveForward`)均为瞬移,不会因延时阻塞程序。 **延时行为:** - `0` 毫秒:瞬间出图,无任何 sleep 开销。 - `1`–`19` 毫秒:将多条线段合并为一次 sleep,以摊薄操作系统调度开销。例如,1 毫秒延时下画 20 条线段后统一睡眠 20 毫秒;10 毫秒延时下画 2 条线段后统一睡眠 20 毫秒。 - `20` 毫秒及以上:每条线段独立等待,延时与设置值完全一致。 在 Windows 10(1803+)及以上,.NET 10.0/.NET Framework 4.8 环境下,延时通过 [QuickTickLib](https://www.nuget.org/packages/QuickTickLib/) 的 IO 完成端口实现高精度定时;在旧版 Windows 或不支持的平台上,自动回退到 `Thread.Sleep`。 ## 主要特性 AOTLogoSharp 提供以下命令和特性: - 基本画笔命令 - PENDOWN/PD - PENUP/PU - SETPENCOLOR/SETPC/PC(整数 0–15 或 RGB 列表 [0–255] 仅正实数,自动取整) - **SETBG** / **SETBACKGROUND** — 设置画布背景色。支持: - 整数 0–15(标准 Logo 颜色索引) - `[R G B]` 三元列表(0–255) - `[R G B A]` 四元列表(带透明度,按该 RGBA 清屏) - SETPENSIZE(非负实数) - PENERASE/PE - PENNORMAL/PN - 基本绘图命令(仅实数) - LEFT/LT(逆时针,仅实数) - RIGHT/RT(顺时针,仅实数) - FORWARD/FD(仅实数) - BACKWARD/BK/BACK(仅实数) - DRAW/CLS/CLEARSCR/CLEARSCREEN/CS - 海龟控制命令 - HOME - SHOWTURTLE/ST - HIDETURTLE/HT - SETX/SETY/SETXY(仅实数) - **SETCOMPLEX** *z* — 一次内置调用把海龟移动到复数 *z* 的 `(Re(z), Im(z))` 坐标。语义上等价于 `SETXY (Re z) (Im z)`,但不需要先把实部 / 虚部分别 `MAKE` 出来再两次 `SETXY`,可以直接把复平面当作单一值处理。对密集型绘图(例如分形)显著更快。示例:`SETCOMPLEX 3 + 4i` 把海龟移动到 `(3, 4)`。接受任何能求值为复数的表达式。 - **SETPOLAR** *ρ θ* — 将海龟移动到极坐标 (ρ, θ),其中 ρ 为半径,θ 为**弧度制**角度。内部计算 `x = ρ cos θ`、`y = ρ sin θ` 后调用 `SETXY`。示例:`SETPOLAR 100 PI` 移动到 `(-100, 0)`,`SETPOLAR 100 PI / 2` 移动到 `(0, 100)`。此命令简化了画圆、玫瑰花线等极坐标曲线图案的绘制。 - SETH/SETHEADING(仅实数;默认 Logo 语义:0° = 正北,顺时针;设置 `logo.HeadingMode = "standard"` 可恢复数学标准:0° = 正东,逆时针) - 流程控制命令 - REPEAT *次数*(非负实数,自动取整)和 RepCount - **FOR** *[变量名 起始值 终止值 步长?]* *[循环体]* — 灵活迭代。控制列表指定变量名、起始值、终止值和可选步长。每次迭代更新变量并执行循环体。 - **实数 FOR**:当 `起始值`、`终止值`、`步长`(若有)均为实数时,循环在 `sign(当前值 - 终止值) != sign(步长)` 时退出。未指定步长时自动取 `1` 或 `-1`,循环体至少执行一次(do-while 语义)。`(终止值 - 起始值) / 步长` 必须为**非负实数**,否则抛出 `RuntimeException`(如 `FOR [T 0 1 -1]` 将被拒绝)。示例:`FOR [I 2 7 1.5] [PRINT :I]` 输出 `2`、`3.5`、`5`、`6.5`。 - **复数 FOR**(仅显式步长):当 `起始值`、`终止值`、`步长` 中任一为复数时,计算 `(终止值 - 起始值) / 步长`——该值必须为非负实数整数 *n*——然后迭代 `i = 0..n`,每次赋值 `当前值 = 起始值 + i * 步长`。实部与虚部同时增加/减少。示例:`FOR [T 0 3+3i 1+1i] [PRINT :T]` 输出 `0`、`1+i`、`2+2i`、`3+3i`。无步长时要求起始值和终止值均为实数(FMSLogo 语义)。 - **IF** *条件* *[真值执行语句]* — 单分支条件判断。条件支持复数:非零(包括非零复数)为真。示例:`IF :x > 0 [PRINT [正数]]`,`IF :z == 3 + 4i [PRINT [匹配]]`。 - **IF** ... **ELSEIF** ... **ELSE** — 多分支条件判断。在 `IF 条件 [语句块]` 之后,可以跟任意数量的 `ELSEIF 条件 [语句块]`,最后可选以 `ELSE [语句块]` 结尾。仅执行第一个条件为真的分支。示例: ```Logo IF :x > 0 [PRINT [正数]] ELSEIF :x = 0 [PRINT [零]] ELSE [PRINT [负数]] ``` - **IF** ... **ELSE** — 双分支条件判断(无 ELSEIF 的简写形式)。示例:`IF :x > 0 [PRINT [正数]] ELSE [PRINT [非正数]]`。 - **IFELSE** *条件* *[真值执行语句]* *[假值执行语句]* — 旧式双分支条件判断(FMSLogo 兼容)。条件为真时执行第一个方括号块,否则执行第二个。示例:`IFELSE :x > 0 [PRINT [正数]] [PRINT [非正数]]`。 - WHILE(条件支持复数) - DELAY(仅实数) - WAIT(仅实数) - STOP(立即退出当前最内层过程) - **OUTPUT/OP/RETURN** — 从当前过程中返回一个值并立即退出。支持三种返回形式: - **标量/复数**:`OUTPUT 42`、`OUTPUT :x + 1`、`OUTPUT :z`(复数)。返回单个 `Scalar` 值,调用方通过 `{PROC args}` 获取。 - **列表字面量 `[expr*]`**:`OUTPUT [255 0 0]`、`OUTPUT [:R :G :B]`。返回 `List`,调用方通过 `MAKE "var {PROC args}` 存储后,可直接用于 `SETPENCOLOR :var` 等命令。示例: ```Logo TO GETRED OUTPUT [255 0 0] END MAKE "c {GETRED} SETPENCOLOR :c ; 等价于 SETPENCOLOR [255 0 0] ``` - 与 STOP 的区别:`STOP` 只结束过程不返回值;`OUTPUT` 结束过程并返回一个值,调用方通过 `MAKE "var {PROC args}` 或表达式上下文获取该值。 - 几何圆弧命令(仅实数,半径非负) - **ARC** *角度 半径* — 以海龟位置为圆心绘制角度(度)和半径的圆弧。**正角度 = 顺时针画弧**,负角度 = 逆时针画弧。起点位于海龟朝向的反方向(朝向 + 180°)。海龟**不移动**。支持**几何擦除**(PE/PN):在 PE 模式下再次绘制同一圆弧可擦除重叠部分。 - **ARC2** *角度 半径* — 圆心位于海龟朝向的**右侧**距离半径处,起点沿海龟朝向方向。**正角度 = 顺时针画弧**,负角度 = 逆时针画弧。海龟**沿弧移动**。同样支持几何擦除。 - **CIRCLE** *半径* — 以海龟位置为圆心绘制完整 360° 圆。等价于 `ARC 360 半径`。 - 文本输出命令 - **PRINT** — 输出文本到控制台/事件(复数格式化为 `a+bi`)。详见 [PRINT 命令](#print-命令与-print-事件)。 - **PRINT FORM** *数字 总宽度 小数位数* — 格式化数字为固定宽度右对齐字符串(复数:精度同时影响实部和虚部)。详见 [FORM 命令](#form-命令)。 - **LABEL** — 在海龟当前位置绘制文本到画布(复数格式化为 `a+bi`)。详见 [LABEL 命令](#label-命令)。**标签始终从左到右渲染**,与 MSWLogo 中文字跟随海龟朝向不同。 - **LABEL FORM** *数字 总宽度 小数位数* — 在画布上绘制格式化数字。详见 [FORM 命令](#form-命令)。 - **SETFONT** — 设置后续 `LABEL` 命令使用的字体(字体名、字号、样式)。详见 [SETFONT 命令](#setfont-命令)。 - 语言特性 - **MAKE 命令(变量赋值)** - 基本形式:`MAKE "变量名 表达式`。将表达式结果存储到变量中,变量名必须以 `"` 开头。 - 支持的右手边形式: - **标量/复数**:`MAKE "x 42`、`MAKE "z 3 + 4i` - **列表字面量 `[expr*]`**:`MAKE "rgb [255 0 0]`。列表中的每个元素被求值为 double 并存储为 `List`,可直接用于 `SETPENCOLOR :rgb`、`SETBG :rgb` 等需要列表参数的命令。 - **函数调用 `{PROC args}`**:`MAKE "color {HOTCOLOR :iter :max}`。当函数(用户定义过程)通过 `OUTPUT [list]` 返回列表时,变量将存储完整的 `List`。 - **表达式**:`MAKE "sum :a + :b`、`MAKE "half :W / 2` - 变量引用:使用 `:变量名` 读取变量值,如 `:x`、`:rgb`、`:sum`。 - 变量作为列表传递:当变量存储的是 `List` 时,引用该变量会自动展开为列表参数。例如 `MAKE "c {GETRED}` 后,`SETPENCOLOR :c` 等价于 `SETPENCOLOR [255 0 0]`。 - **复数支持** - 虚数字面量:数字后紧跟 `i`,如 `4i`、`2.5i`、`1i`。虚数单位需写 `1i`(独立的 `i` 被视为变量名,避免与 `FOR [I 1 10]` 等冲突)。 - 复数通过实部 + 虚部构成:`MAKE "z 3 + 4i`(即 3+4i)。 - double 实数与复数运算时,自动提升为 Complex;计算结果虚部 < 1×10⁻¹⁴ 时自动退化为 double。 - 向仅实数命令/运算符传入复数时,抛出带精确 `[行:列]` 位置的 `RuntimeException`。 - 表达式 - 算术运算(支持复数):`+`、`-`、`*`、`/`、`^` - 算术运算(仅实数):`%`(取余) - 比较运算(支持复数):`==`、`=`、`<>` - 比较运算(仅实数):`<`、`>`、`<=`、`>=` - 逻辑运算(支持复数):NOT - 逻辑运算(仅实数):AND、OR - 花括号表达式:`{expr}`(可直接计算,不必是函数名) - 过程(Procedures) - 函数调用 - SQRT(支持复数输入,负实数自动返回复数) - RANDOM(仅实数) - **三角函数(角度制,仅实数)**:SIN/COS/TAN/COT/SEC/CSC - **反三角函数(返回度,仅实数)**:ASIN/ACOS/ATAN/ACOT/ASEC/ACSC,别名:ARCSIN/ARCCOS/ARCTAN/ARCCOT/ARCSEC/ARCCSC - **三角函数(弧度制,支持复数,FMSLogo 兼容)**:RADSIN/RADCOS/RADTAN/RADCOT/RADSEC/RADCSC - **反三角函数(返回弧度,支持复数,FMSLogo 兼容)**:RADASIN/RADACOS/RADATAN/RADACOT/RADASEC/RADACSC,别名:RADARCSIN/RADARCCOS/RADARCTAN/RADARCCOT/RADARCSEC/RADARCCSC - **双曲函数(弧度制,支持复数)**:SINH/COSH/TANH/COTH/SECH/CSCH - **反双曲函数(返回弧度,支持复数)**:ASINH/ACOSH/ATANH/ACOTH/ASECH/ACSCH,别名:ARCSINH/ARCCOSH/ARCTANH/ARCCOTH/ARCSECH/ARCCSCH - **特殊函数**:TGAMMA(Γ 函数)/LGAMMA(ln|Γ|)/ERF/ERFC/ERFCX。**注意:TGAMMA / LGAMMA / ERF / ERFC / ERFCX 仅支持实数输入**(传入复数会抛出 `RuntimeException`) - ABS(支持复数,返回模;别名:MAGNITUDE) - POWER(支持复数底数和/或指数) - EXP(支持复数输入) - LOG(自然对数,支持复数输入,负实数自动返回复数)/LOG10(支持复数输入),别名:LN - **REAL** *z* — 提取复数实部。对实数直接返回自身。别名:**RE**。 - **IMAGINARY** *z* — 提取复数虚部系数。对实数返回 0。别名:**IM**。 - **PHASE** *z* — 复数辐角(返回弧度)。对正实数返回 0,负实数返回 π。别名:**ARG**。 - **CONJUGATE** *z* — 复数共轭。对实数直接返回自身。别名:**CONJ**。 - **SE/WORD/LIST**(`(SE 1 2 3)` 多参形式) - **FORM** *数字 总宽度 小数位数* — 将数字(或复数)格式化为固定宽度右对齐字符串(FMSLogo 兼容)。复数:精度同时影响实部和虚部,总宽度为复数字符串总长 + 补空格数。可独立用于 `{FORM 3.14 6 2}`、跟在 PRINT 后 `PRINT FORM 3.14 6 2`、或用于 `{FORM :var 4 1}`。 - **SIGN** *x*(仅实数)— 返回符号:-1(负数)、0(零)、1(正数)。 - **ROUND** *x*(仅实数)— 四舍五入到最接近的整数(中点舍入到最近的偶数)。 - **FLOOR** *x*(仅实数)— 向下取整到不大于 x 的最大整数。 - **CEIL** *x*(仅实数)— 向上取整到不小于 x 的最小整数。 - **TRUNC** *x*(仅实数)— 向零方向截断,丢弃小数部分。别名:**INT**。 - **PERM** *n k*(仅实数)— 排列数 n! / (n-k)!。当 n、k 均为非负整数时返回整数结果;当任一参数为非整数(如 5.5、2.0)时走对数 Γ 函数路径,返回更精确的双精度浮点结果(FMSLogo 兼容行为)。示例:`{PERM 5 2}` → `20`,`{PERM 6 0}` → `1`。 - **COMB** *n k*(仅实数)— 组合数 n! / (k! (n-k)!) = PERM(n, k) / k!。当 n、k 均为非负整数时返回整数结果;当任一参数为非整数时走对数 Γ 函数路径(伽马函数计算)。示例:`{COMB 5 2}` → `10`,`{COMB 6 3}` → `20`。 - **LAMBERTW** *k z*(支持复数)— 朗伯 W 函数(乘积对数),满足 `z = W(z) · e^{W(z)}`。*k* 为分支指数(0 = 主分支,-1 = 下分支;复数 *z* 支持其他整数 *k*)。实数输入使用 Halley 迭代法(精度约 1e-15);复数输入走 .NET 内置 Complex 优化路径。别名:**W**、**PRODUCTLOG**。示例:`{LAMBERTW 0 1}` → Ω ≈ 0.5671433(Omega 常数),`{LAMBERTW 0 e}` → 1,`{W 0 1+i}`,`{PRODUCTLOG -1 -0.1}`。 - **REMAINDER** *被除数 除数*(仅实数)— 返回被除数除以除数的余数。余数符号与被除数一致(如 `{REMAINDER -7 3}` → `-1`)。别名:**REM**。等价中缀运算符:`%`(如 `17 % 5` → `2`)。 - **MAX** *a b ...*(仅实数,变参)— 返回所有参数中的最大值。支持 1 至 N 个参数:单参直接返回,双参用 `Math.Max`,三参及以上用 `Linq.Max`。示例:`{MAX 3 7}` → `7`,`{MAX 1 5 3}` → `5`,`{MAX {ABS -4} {ABS 2}}` → `4`。 - **MIN** *a b ...*(仅实数,变参)— 返回所有参数中的最小值。支持 1 至 N 个参数:单参直接返回,双参用 `Math.Min`,三参及以上用 `Linq.Min`。示例:`{MIN 3 7}` → `3`,`{MIN 1 5 3}` → `1`,`{MIN {ABS -4} {ABS 2}}` → `2`。 - 行内注释 - 内置数学常量 - PI - E > **单参函数与多参函数的参数语法**: > - **单参函数**(如 `{EXP expr}`、`{SQRT expr}`、`{LGAMMA expr}`):后面的表达式可以包含二元运算符,如 `{EXP :a + :b}` 等价于 `EXP(a + b)`。 > - **多参函数**(如 `{POWER base exp}`、`{PERM n k}`、`{COMB n k}`、`{MAX a b}`):参数以空格分隔。若某个参数本身包含二元运算符或一元负号,需要用括号括起来,如 `{POWER 2 (-:X)}` 表示 `POWER(2, -X)`。负数字面量(如 `-1`)无需括号——词法分析器会将其作为一个完整的数字识别。 > **重要:所有函数调用(包括内置函数和 TO...END 自定义过程)必须用花括号 `{ }` 包裹。** > 例如 `{SQRT 2}`、`{tpdf 1 5}`。不带花括号会导致解析歧义,AOTLogoSharp 会报"Expected expression"错误。 > 自定义过程调用也不例外:**必须先 `TO procname :a :b ... END` 定义,然后用 `{procname 1 2}` 调用**。 > SETXY 的参数中如需调用自定义过程,也必须加花括号:`SETXY -100 {catenary 1 2} * 40`。 ## API 破坏性变更 (v1.3 → v1.4) 1. **`ConstantExpressionNode`**(`public` 类)的 `Value` 属性类型由 `float` 改为 `double`,构造函数参数同步变更。 2. 所有数值计算现以 **double 精度**执行(符合 UCBLogo/MSWLogo 规范),不再做单精度截断。 3. **许可证** 由 MIT 变更为 **BSD-3-Clause**(`THIRD-PARTY-NOTICE.txt` 列出了 NetTopologySuite、SkiaSharp、PolySharp、QuickTickLib 等第三方许可)。 ## API 破坏性变更 (v1.5 → v1.6) 1. **复数支持**:`Evaluation.Value` 类型由 `double` 变更为 `Scalar`(24 字节值类型 struct)。所有内部数值运算现使用 `Scalar` 代替 `object`,消除装箱开销。 2. **Trampoline 执行引擎**:用户定义过程调用现使用堆上 Trampoline 循环替代 C# 递归。`MaxCallStackDepth`(默认 500)现在在堆栈上检查,可以设置更高值而不会导致真正的 `StackOverflowException`。嵌套块(REPEAT/WHILE/IF)中的 STOP/OUTPUT 仍在内部使用异常控制流,但在 Trampoline 边界被捕获。 3. **新增目标框架**:新增 `netstandard2.1` TFM。`System.Memory` 仅在 `netstandard2.0` / `net48` 下引用。 ## 局限性 - 代码编辑不支持多行格式 - 除上述列出的命令外,其他 Logo 命令暂不支持。未来会逐步添加 - **函数调用(自定义过程和内置函数)必须用花括号包裹**,例如 `{SQRT 2}` 或 `{tpdf 1 5}` - `PRINT "text` 读取字面量直到下一个空白或分隔符(括号 / 方括号)为止;要在输出文本中包含空格,请改用列表字面量:`PRINT [Hello World]` - 引号标识符(`"name`)与变量 / 关键字的匹配都是大小写不敏感的;但 `PRINT "text` 字面量内部的内容会按用户输入的原始大小写原样输出。 - **LABEL 文本始终从左到右渲染**在画布上,不受海龟朝向影响。这与 MSWLogo 中标签文字跟随海龟旋转的行为不同。 ## 无头模式 / CI 批量图片生成 `LogoSharp.Main` 同时也是**无头批量渲染器**。传入 `.logo` 文件路径(支持 `*` 和 `?` 通配符),就会跳过 Avalonia GUI,自动将每个文件走完整 Logo → Turtle → PNG 流水线: ```bash # 渲染单个文件 LogoSharp.Main.exe samples/house.logo # 批量渲染所有样例(支持通配符) LogoSharp.Main.exe samples/*.logo # 渲染指定子集 LogoSharp.Main.exe samples/house*.logo samples/test-*.logo ``` **工作原理**(参见 [`HeadlessMode.cs`](src/LogoSharp.Main/HeadlessMode.cs)): 1. 按文件名排序枚举所有匹配的 `.logo` 文件。 2. 每个文件创建全新 `Turtle` 实例(天然隔离,无跨文件状态泄漏)。 3. 将所有 `Logo` 事件(FD、BK、RT、LT、SETPC、LABEL、SETFONT 等)挂到 turtle 上——**与 Avalonia GUI 完全相同的订阅**。 4. 调用 `logo.Execute(sourceCode)` 再 `turtle.CreateRenderSurface()` → 编码为 PNG。 5. PNG 保存到 exe 同级目录的 `headless-output//` 下,每个 `.logo` 对应一个 PNG。 6. 向 stdout 逐文件打印 `[OK/FAIL]` 行,末尾输出总绘制耗时统计。仅当所有文件都成功时退出码为 0;任一文件失败(解析错误、运行时错误或 I/O 错误)则退出码非零。 **CI 集成**:项目自身的 [GitHub Actions 工作流](.github/workflows/build.yml) 将 `LogoSharp.Main` 发布为 Native AOT 可执行文件,并把全量 `samples/*.logo` 套件作为 CI 步骤执行——在每次 push 和 PR 时验证所有样例仍能正确渲染。 **内存安全**:`MemoryGuard` 看门狗(默认 1024 MB 阈值)在无头运行期间监控进程,若检测到解析器死循环或内存失控分配则立即终止进程。 ## WinForm 示例(已废弃) 位于 `src-winform/LogoSharp.Drawing/` 和 `src-winform/LogoSharp.Main/` 的 WinForm 示例项目**不再维护更新**,仅作为历史参考保留。新开发请使用 **Avalonia UI** 示例,位于 `src/LogoSharp.Main/`。 ## 许可证 BSD-3-Clause。详见 [LICENSE](LICENSE)。第三方依赖许可详见 [THIRD-PARTY-NOTICE.txt](THIRD-PARTY-NOTICE.txt)。 ## 捐赠 [![pmBoq4e.md.jpg](https://s41.ax1x.com/2026/07/06/pmBoq4e.md.jpg)](https://imgchr.com/i/pmBoq4e) [![pmBTSDP.md.png](https://s41.ax1x.com/2026/07/06/pmBTSDP.md.png)](https://imgchr.com/i/pmBTSDP)