diff --git a/BuildTime.txt b/BuildTime.txt index 6f96d89..82284af 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-29 19:08:01 \ No newline at end of file +2026-03-29 19:34:16 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index e2f888f..1097cc6 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -78bda7c \ No newline at end of file +3a9ff9c \ No newline at end of file diff --git a/Gui/AppManager.cs b/Gui/AppManager.cs index 1c5652f..95d2a5b 100644 --- a/Gui/AppManager.cs +++ b/Gui/AppManager.cs @@ -142,6 +142,7 @@ namespace CMLeonOS.Gui RegisterApp(new AppMetadata("Environment Variables", () => { return new EnvironmentVariables(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("UILib Gallery", () => { return new UILibGallery(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("Music Editor", () => { return new MusicEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); + RegisterApp(new AppMetadata("Function Grapher", () => { return new FunctionGrapher(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); Logger.Logger.Instance.Info("AppManager", $"{AppMetadatas.Count} apps were registered."); diff --git a/Gui/Apps/FunctionGrapher.cs b/Gui/Apps/FunctionGrapher.cs new file mode 100644 index 0000000..ccf7f02 --- /dev/null +++ b/Gui/Apps/FunctionGrapher.cs @@ -0,0 +1,533 @@ +using CMLeonOS; +using CMLeonOS.Gui.UILib; +using System; +using System.Drawing; + +namespace CMLeonOS.Gui.Apps +{ + internal class FunctionGrapher : Process + { + internal FunctionGrapher() : base("Function Grapher", ProcessType.Application) { } + + private AppWindow window; + private readonly WindowManager wm = ProcessManager.GetProcess(); + + private TextBox expressionBox; + private NumericUpDown scaleInput; + private TextBox statusBox; + private Window canvas; + + private string currentExpression = "sin(x)"; + private readonly Color canvasBg = Color.FromArgb(250, 252, 255); + private readonly Color axisColor = Color.FromArgb(156, 170, 188); + private readonly Color gridColor = Color.FromArgb(226, 233, 244); + private readonly Color curveColor = Color.FromArgb(44, 102, 217); + + private void Layout() + { + int top = 34; + int side = 8; + int inputH = 24; + int statusH = 24; + + expressionBox.MoveAndResize(side, top, window.Width - 200, inputH); + scaleInput.MoveAndResize(window.Width - 184, top, 86, inputH); + + int buttonY = top; + int buttonW = 40; + int buttonX = window.Width - 92; + + int canvasY = top + inputH + 8; + int canvasH = window.Height - canvasY - statusH - 10; + canvas.MoveAndResize(side, canvasY, window.Width - (side * 2), canvasH); + statusBox.MoveAndResize(side, window.Height - statusH - 6, window.Width - (side * 2), statusH); + } + + private void DrawGridAndAxes(int centerX, int centerY, int pixelPerUnit) + { + if (pixelPerUnit < 4) + { + pixelPerUnit = 4; + } + + for (int x = centerX; x < canvas.Width; x += pixelPerUnit) + { + canvas.DrawLine(x, 0, x, canvas.Height - 1, gridColor); + } + for (int x = centerX; x >= 0; x -= pixelPerUnit) + { + canvas.DrawLine(x, 0, x, canvas.Height - 1, gridColor); + } + for (int y = centerY; y < canvas.Height; y += pixelPerUnit) + { + canvas.DrawLine(0, y, canvas.Width - 1, y, gridColor); + } + for (int y = centerY; y >= 0; y -= pixelPerUnit) + { + canvas.DrawLine(0, y, canvas.Width - 1, y, gridColor); + } + + canvas.DrawLine(0, centerY, canvas.Width - 1, centerY, axisColor); + canvas.DrawLine(centerX, 0, centerX, canvas.Height - 1, axisColor); + } + + private void RenderGraph() + { + if (canvas.Width < 8 || canvas.Height < 8) + { + return; + } + + canvas.Clear(canvasBg); + int centerX = canvas.Width / 2; + int centerY = canvas.Height / 2; + int pixelPerUnit = Math.Max(8, scaleInput.Value); + + DrawGridAndAxes(centerX, centerY, pixelPerUnit); + + ExpressionParser parser = new ExpressionParser(currentExpression); + if (!parser.Parse()) + { + statusBox.Text = "Parse error: " + parser.ErrorMessage; + statusBox.Render(); + wm.Update(statusBox); + wm.Update(canvas); + return; + } + + bool hasLast = false; + int lastX = 0; + int lastY = 0; + + for (int sx = 0; sx < canvas.Width; sx++) + { + double x = (sx - centerX) / (double)pixelPerUnit; + double y; + if (!parser.TryEvaluate(x, out y)) + { + hasLast = false; + continue; + } + + if (double.IsNaN(y) || double.IsInfinity(y)) + { + hasLast = false; + continue; + } + + int sy = centerY - (int)(y * pixelPerUnit); + if (sy < -10000 || sy > 10000) + { + hasLast = false; + continue; + } + + if (hasLast) + { + if (Math.Abs(sy - lastY) < canvas.Height) + { + canvas.DrawLine(lastX, lastY, sx, sy, curveColor); + } + } + hasLast = true; + lastX = sx; + lastY = sy; + } + + statusBox.Text = "OK: y = " + currentExpression; + statusBox.Render(); + wm.Update(statusBox); + wm.Update(canvas); + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 140, 80, 900, 560); + window.Title = "Function Grapher"; + window.Icon = AppManager.DefaultAppIcon; + window.CanResize = true; + window.UserResized = () => + { + Layout(); + RenderGraph(); + }; + window.Closing = TryStop; + wm.AddWindow(window); + + expressionBox = new TextBox(window, 8, 34, 680, 24); + expressionBox.Text = currentExpression; + expressionBox.PlaceholderText = "Enter expression, e.g. sin(x), x^2+2*x+1"; + expressionBox.Submitted = () => + { + currentExpression = expressionBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(currentExpression)) + { + currentExpression = "x"; + expressionBox.Text = currentExpression; + } + RenderGraph(); + }; + wm.AddWindow(expressionBox); + + scaleInput = new NumericUpDown(window, 696, 34, 86, 24); + scaleInput.Minimum = 8; + scaleInput.Maximum = 80; + scaleInput.Value = 24; + scaleInput.Step = 2; + scaleInput.Changed = (_) => RenderGraph(); + wm.AddWindow(scaleInput); + + Button plotButton = new Button(window, 790, 34, 46, 24); + plotButton.Text = "Plot"; + plotButton.OnClick = (_, _) => + { + currentExpression = expressionBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(currentExpression)) + { + currentExpression = "x"; + expressionBox.Text = currentExpression; + } + RenderGraph(); + }; + wm.AddWindow(plotButton); + + Button clearButton = new Button(window, 842, 34, 50, 24); + clearButton.Text = "Clear"; + clearButton.OnClick = (_, _) => + { + expressionBox.Text = "x"; + currentExpression = "x"; + RenderGraph(); + }; + wm.AddWindow(clearButton); + + canvas = new Window(this, window, 8, 66, window.Width - 16, window.Height - 98); + wm.AddWindow(canvas); + + statusBox = new TextBox(window, 8, window.Height - 30, window.Width - 16, 24); + statusBox.ReadOnly = true; + statusBox.Text = "Ready"; + wm.AddWindow(statusBox); + + Layout(); + RenderGraph(); + wm.Update(window); + } + + public override void Run() + { + } + + private class ExpressionParser + { + private readonly string text; + private int pos; + private Node root; + + internal string ErrorMessage { get; private set; } = "Unknown"; + + internal ExpressionParser(string expr) + { + text = expr ?? string.Empty; + pos = 0; + } + + internal bool Parse() + { + try + { + pos = 0; + root = ParseExpression(); + SkipSpaces(); + if (pos != text.Length) + { + ErrorMessage = "Unexpected token near position " + pos; + return false; + } + return root != null; + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + return false; + } + } + + internal bool TryEvaluate(double x, out double y) + { + y = 0; + if (root == null) + { + return false; + } + + try + { + y = root.Eval(x); + return true; + } + catch + { + return false; + } + } + + private void SkipSpaces() + { + while (pos < text.Length && char.IsWhiteSpace(text[pos])) + { + pos++; + } + } + + private bool Match(char c) + { + SkipSpaces(); + if (pos < text.Length && text[pos] == c) + { + pos++; + return true; + } + return false; + } + + private string ParseIdentifier() + { + SkipSpaces(); + int start = pos; + while (pos < text.Length && (char.IsLetter(text[pos]) || char.IsDigit(text[pos]) || text[pos] == '_')) + { + pos++; + } + if (start == pos) + { + return string.Empty; + } + return text.Substring(start, pos - start); + } + + private Node ParseNumber() + { + SkipSpaces(); + int start = pos; + bool dot = false; + while (pos < text.Length) + { + char c = text[pos]; + if (char.IsDigit(c)) + { + pos++; + continue; + } + if (c == '.' && !dot) + { + dot = true; + pos++; + continue; + } + break; + } + + if (start == pos) + { + return null; + } + + double value = double.Parse(text.Substring(start, pos - start)); + return new NumberNode(value); + } + + private Node ParsePrimary() + { + SkipSpaces(); + + if (Match('(')) + { + Node inner = ParseExpression(); + if (!Match(')')) + { + throw new Exception("Missing ')'"); + } + return inner; + } + + if (Match('+')) + { + return ParsePrimary(); + } + if (Match('-')) + { + return new UnaryNode('-', ParsePrimary()); + } + + Node number = ParseNumber(); + if (number != null) + { + return number; + } + + string ident = ParseIdentifier(); + if (ident.Length > 0) + { + string id = ident.ToLower(); + if (id == "x") + { + return new VarNode(); + } + if (id == "pi") + { + return new NumberNode(Math.PI); + } + if (id == "e") + { + return new NumberNode(Math.E); + } + + if (!Match('(')) + { + throw new Exception("Function call expected for '" + ident + "'"); + } + Node arg = ParseExpression(); + if (!Match(')')) + { + throw new Exception("Missing ')' after function " + ident); + } + return new FuncNode(id, arg); + } + + throw new Exception("Unexpected token at " + pos); + } + + private Node ParsePow() + { + Node left = ParsePrimary(); + SkipSpaces(); + if (Match('^')) + { + Node right = ParsePow(); + return new BinNode('^', left, right); + } + return left; + } + + private Node ParseMulDiv() + { + Node left = ParsePow(); + while (true) + { + SkipSpaces(); + if (Match('*')) + { + left = new BinNode('*', left, ParsePow()); + } + else if (Match('/')) + { + left = new BinNode('/', left, ParsePow()); + } + else + { + break; + } + } + return left; + } + + private Node ParseExpression() + { + Node left = ParseMulDiv(); + while (true) + { + SkipSpaces(); + if (Match('+')) + { + left = new BinNode('+', left, ParseMulDiv()); + } + else if (Match('-')) + { + left = new BinNode('-', left, ParseMulDiv()); + } + else + { + break; + } + } + return left; + } + + private abstract class Node + { + internal abstract double Eval(double x); + } + + private class NumberNode : Node + { + private readonly double v; + internal NumberNode(double value) { v = value; } + internal override double Eval(double x) { return v; } + } + + private class VarNode : Node + { + internal override double Eval(double x) { return x; } + } + + private class UnaryNode : Node + { + private readonly char op; + private readonly Node arg; + internal UnaryNode(char oper, Node node) { op = oper; arg = node; } + internal override double Eval(double x) + { + double v = arg.Eval(x); + return op == '-' ? -v : v; + } + } + + private class BinNode : Node + { + private readonly char op; + private readonly Node a; + private readonly Node b; + internal BinNode(char oper, Node left, Node right) { op = oper; a = left; b = right; } + internal override double Eval(double x) + { + double lv = a.Eval(x); + double rv = b.Eval(x); + switch (op) + { + case '+': return lv + rv; + case '-': return lv - rv; + case '*': return lv * rv; + case '/': return rv == 0 ? double.NaN : lv / rv; + case '^': return Math.Pow(lv, rv); + default: return double.NaN; + } + } + } + + private class FuncNode : Node + { + private readonly string fn; + private readonly Node arg; + internal FuncNode(string name, Node node) { fn = name; arg = node; } + internal override double Eval(double x) + { + double v = arg.Eval(x); + switch (fn) + { + case "sin": return Math.Sin(v); + case "cos": return Math.Cos(v); + case "tan": return Math.Tan(v); + case "abs": return Math.Abs(v); + case "sqrt": return v < 0 ? double.NaN : Math.Sqrt(v); + case "log": return v <= 0 ? double.NaN : Math.Log(v); + case "exp": return Math.Exp(v); + default: return double.NaN; + } + } + } + } + } +}