diff --git a/OpenNest.Core/Math/ExpressionEvaluator.cs b/OpenNest.Core/Math/ExpressionEvaluator.cs
new file mode 100644
index 0000000..591aa28
--- /dev/null
+++ b/OpenNest.Core/Math/ExpressionEvaluator.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace OpenNest.Math
+{
+ ///
+ /// Recursive descent parser for simple arithmetic expressions supporting
+ /// +, -, *, /, parentheses, unary minus/plus, and $variable references.
+ ///
+ public static class ExpressionEvaluator
+ {
+ public static double Evaluate(string expression, IReadOnlyDictionary variables)
+ {
+ var parser = new Parser(expression, variables);
+ var result = parser.ParseExpression();
+ parser.SkipWhitespace();
+ if (!parser.IsEnd)
+ throw new FormatException($"Unexpected character at position {parser.Position}: '{parser.Current}'");
+ return result;
+ }
+
+ private ref struct Parser
+ {
+ private readonly ReadOnlySpan _input;
+ private readonly IReadOnlyDictionary _variables;
+ private int _pos;
+
+ public Parser(string input, IReadOnlyDictionary variables)
+ {
+ _input = input.AsSpan();
+ _variables = variables;
+ _pos = 0;
+ }
+
+ public int Position => _pos;
+ public bool IsEnd => _pos >= _input.Length;
+ public char Current => _input[_pos];
+
+ public void SkipWhitespace()
+ {
+ while (_pos < _input.Length && _input[_pos] == ' ')
+ _pos++;
+ }
+
+ // Expression = Term (('+' | '-') Term)*
+ public double ParseExpression()
+ {
+ SkipWhitespace();
+ var left = ParseTerm();
+
+ while (true)
+ {
+ SkipWhitespace();
+ if (IsEnd) break;
+
+ var op = Current;
+ if (op != '+' && op != '-') break;
+
+ _pos++;
+ SkipWhitespace();
+ var right = ParseTerm();
+
+ left = op == '+' ? left + right : left - right;
+ }
+
+ return left;
+ }
+
+ // Term = Unary (('*' | '/') Unary)*
+ private double ParseTerm()
+ {
+ var left = ParseUnary();
+
+ while (true)
+ {
+ SkipWhitespace();
+ if (IsEnd) break;
+
+ var op = Current;
+ if (op != '*' && op != '/') break;
+
+ _pos++;
+ SkipWhitespace();
+ var right = ParseUnary();
+
+ left = op == '*' ? left * right : left / right;
+ }
+
+ return left;
+ }
+
+ // Unary = ('-' | '+')? Primary
+ private double ParseUnary()
+ {
+ SkipWhitespace();
+ if (!IsEnd && Current == '-')
+ {
+ _pos++;
+ return -ParsePrimary();
+ }
+ if (!IsEnd && Current == '+')
+ {
+ _pos++;
+ }
+ return ParsePrimary();
+ }
+
+ // Primary = '(' Expression ')' | '$' Identifier | Number
+ private double ParsePrimary()
+ {
+ SkipWhitespace();
+
+ if (IsEnd)
+ throw new FormatException("Unexpected end of expression.");
+
+ if (Current == '(')
+ {
+ _pos++; // consume '('
+ var value = ParseExpression();
+ SkipWhitespace();
+ if (IsEnd || Current != ')')
+ throw new FormatException("Expected closing parenthesis.");
+ _pos++; // consume ')'
+ return value;
+ }
+
+ if (Current == '$')
+ {
+ _pos++; // consume '$'
+ var start = _pos;
+ while (_pos < _input.Length && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_'))
+ _pos++;
+ if (_pos == start)
+ throw new FormatException("Expected variable name after '$'.");
+ var name = _input.Slice(start, _pos - start).ToString();
+ if (!_variables.TryGetValue(name, out var varValue))
+ throw new KeyNotFoundException($"Undefined variable: ${name}");
+ return varValue;
+ }
+
+ // Number
+ var numStart = _pos;
+ while (_pos < _input.Length && (char.IsDigit(_input[_pos]) || _input[_pos] == '.'))
+ _pos++;
+
+ if (_pos == numStart)
+ throw new FormatException($"Unexpected character '{Current}' at position {_pos}.");
+
+ var numSpan = _input.Slice(numStart, _pos - numStart).ToString();
+ if (!double.TryParse(numSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out var number))
+ throw new FormatException($"Invalid number: '{numSpan}'");
+
+ return number;
+ }
+ }
+ }
+}
diff --git a/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.csproj b/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.csproj
index 740fe7b..1b60598 100644
--- a/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.csproj
+++ b/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.csproj
@@ -11,6 +11,6 @@
..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\
-
+
diff --git a/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs b/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs
new file mode 100644
index 0000000..067354d
--- /dev/null
+++ b/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+using OpenNest.Math;
+
+namespace OpenNest.Tests.Math;
+
+public class ExpressionEvaluatorTests
+{
+ private static readonly Dictionary Empty = new();
+
+ [Fact]
+ public void Evaluate_NumericLiteral()
+ {
+ Assert.Equal(42.0, ExpressionEvaluator.Evaluate("42", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_DecimalLiteral()
+ {
+ Assert.Equal(0.3, ExpressionEvaluator.Evaluate("0.3", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_NegativeLiteral()
+ {
+ Assert.Equal(-5.0, ExpressionEvaluator.Evaluate("-5", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_Addition()
+ {
+ Assert.Equal(5.0, ExpressionEvaluator.Evaluate("2 + 3", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_Subtraction()
+ {
+ Assert.Equal(1.0, ExpressionEvaluator.Evaluate("3 - 2", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_Multiplication()
+ {
+ Assert.Equal(6.0, ExpressionEvaluator.Evaluate("2 * 3", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_Division()
+ {
+ Assert.Equal(2.5, ExpressionEvaluator.Evaluate("5 / 2", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_OperatorPrecedence()
+ {
+ Assert.Equal(7.0, ExpressionEvaluator.Evaluate("1 + 2 * 3", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_Parentheses()
+ {
+ Assert.Equal(9.0, ExpressionEvaluator.Evaluate("(1 + 2) * 3", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_VariableReference()
+ {
+ var vars = new Dictionary { { "diameter", 0.3 } };
+ Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars));
+ }
+
+ [Fact]
+ public void Evaluate_VariableInExpression()
+ {
+ var vars = new Dictionary { { "diameter", 0.6 } };
+ Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter / 2", vars));
+ }
+
+ [Fact]
+ public void Evaluate_MultipleVariables()
+ {
+ var vars = new Dictionary { { "x", 2.0 }, { "y", 3.0 } };
+ Assert.Equal(5.0, ExpressionEvaluator.Evaluate("$x + $y", vars));
+ }
+
+ [Fact]
+ public void Evaluate_CaseInsensitiveVariables()
+ {
+ var vars = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Diameter", 0.3 }
+ };
+ Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars));
+ }
+
+ [Fact]
+ public void Evaluate_UndefinedVariable_Throws()
+ {
+ Assert.Throws(() =>
+ ExpressionEvaluator.Evaluate("$missing", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_NestedParentheses()
+ {
+ Assert.Equal(20.0, ExpressionEvaluator.Evaluate("((2 + 3) * (1 + 1)) * 2", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_UnaryMinusBeforeParens()
+ {
+ Assert.Equal(-6.0, ExpressionEvaluator.Evaluate("-(2 + 4)", Empty));
+ }
+
+ [Fact]
+ public void Evaluate_WhitespaceHandling()
+ {
+ Assert.Equal(5.0, ExpressionEvaluator.Evaluate(" 2 + 3 ", Empty));
+ }
+}