add midpoint and circumcenter functionality
5 files changed, 124 insertions(+), 18 deletions(-)

M README.md
M src/lang/functions.rs
M src/lang/types.rs
M src/lexer.rs
M src/utils/geometry.rs
M README.md +14 -0
@@ 45,6 45,13 @@ geometric functions:
 
 The `point` function creates a point with the given x and y coordinates in the first and second parameters respectively.
 
+### `circumcenter`
+```lisp
+(circumcenter [Triangle]) -> Point
+```
+
+The `circumcenter` function takes in a triangle and returns the circumcenter of that triangle.
+
 ### `orthocenter`
 ```lisp
 (orthocenter [Triangle]) -> Point

          
@@ 73,6 80,13 @@ The `incenter` function takes in a trian
 
 The `lineseg` function creates a line segment with the given two points as the endpoints.
 
+### `midpoint`
+```lisp
+(midpoint [Point] [Point]) -> Point
+```
+
+The `midpoint` function returns a point that is the midpoint of the two given points.
+
 ### `triangle`
 ```lisp
 (triangle [Point] [Point] [Point]) -> Triangle

          
M src/lang/functions.rs +64 -18
@@ 1,7 1,7 @@ 
 use crate::interpreter::is_valid_variable;
 use crate::lang::types::Angle;
 use crate::lang::types::{Circle, Lineseg, Operation, Point, Triangle, Value};
-use crate::utils::geometry::distance;
+use crate::utils::geometry::{distance, midpoint};
 
 /// Macro to implement cloning a boxed trait object
 macro_rules! clone_impl {

          
@@ 21,7 21,7 @@ pub struct FnSet;
 impl Operation for FnSet {
     clone_impl!(FnSet);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("setq requires exactly 2 arguments".to_string());
         }
         let var_name = match &args[0] {

          
@@ 44,7 44,7 @@ pub struct FnAdd;
 impl Operation for FnAdd {
     clone_impl!(FnAdd);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Add requires exactly 2 arguments".to_string());
         }
         match (&args[0], &args[1]) {

          
@@ 60,7 60,7 @@ pub struct FnSub;
 impl Operation for FnSub {
     clone_impl!(FnSub);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Sub requires exactly 2 arguments".to_string());
         }
         match (&args[0], &args[1]) {

          
@@ 76,7 76,7 @@ pub struct FnMul;
 impl Operation for FnMul {
     clone_impl!(FnMul);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Mul requires exactly 2 arguments".to_string());
         }
         match (&args[0], &args[1]) {

          
@@ 92,7 92,7 @@ pub struct FnDiv;
 impl Operation for FnDiv {
     clone_impl!(FnDiv);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Div requires exactly 2 arguments".to_string());
         }
         match (&args[0], &args[1]) {

          
@@ 185,7 185,7 @@ impl FnAngle {
     /// Case 1: create an angle from three points
     fn from_points(&self, args: &[Value]) -> Result<Value, String> {
         // check for 3 arguments
-        if args.len() < 3 {
+        if args.len() != 3 {
             return Err("Angle requires exactly 3 arguments".to_string());
         }
 

          
@@ 224,7 224,7 @@ impl FnLineseg {
     /// Case 1: create a line segment from two points
     fn from_points(&self, args: &[Value]) -> Result<Value, String> {
         // check for 2 arguments
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Line segment requires exactly 2 arguments".to_string());
         }
 

          
@@ 256,12 256,58 @@ impl Operation for FnLineseg {
 }
 
 #[derive(Clone)]
+pub struct FnMidpoint;
+impl Operation for FnMidpoint {
+    clone_impl!(FnMidpoint);
+    fn call(&self, args: &[Value]) -> Result<Value, String> {
+        // check for 2 arguments
+        if args.len() != 2 {
+            return Err("Midpoint requires exactly 2 arguments".to_string());
+        }
+
+        // Extract the two points from the arguments
+        let p1 = match &args[0] {
+            Value::Point(p) => p.clone(),
+            _ => return Err("Invalid type for first argument, expected a Point".to_string()),
+        };
+        let p2 = match &args[1] {
+            Value::Point(p) => p.clone(),
+            _ => return Err("Invalid type for second argument, expected a Point".to_string()),
+        };
+
+        // try getting the midpoint
+        return Ok(Value::Point(midpoint(p1, p2)));
+    }
+}
+
+#[derive(Clone)]
+pub struct FnCircumcenter;
+impl Operation for FnCircumcenter {
+    clone_impl!(FnCircumcenter);
+    fn call(&self, args: &[Value]) -> Result<Value, String> {
+        // check for 1 argument
+        if args.len() != 1 {
+            return Err("Circumcenter requires exactly 1 argument".to_string());
+        }
+
+        // check for 1 triangle
+        let triangle = match &args[0] {
+            Value::Triangle(t) => t.clone(),
+            _ => return Err("Invalid types for triangle".to_string()),
+        };
+
+        // try getting the circumcenter
+        return Ok(Value::Point(triangle.circumcenter()));
+    }
+}
+
+#[derive(Clone)]
 pub struct FnIncenter;
 impl Operation for FnIncenter {
     clone_impl!(FnIncenter);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Incenter requires exactly 1 argument".to_string());
         }
 

          
@@ 282,7 328,7 @@ impl Operation for FnOrthocenter {
     clone_impl!(FnOrthocenter);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Orthocenter requires exactly 1 argument".to_string());
         }
 

          
@@ 303,7 349,7 @@ impl Operation for FnCentroid {
     clone_impl!(FnCentroid);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Centroid requires exactly 1 argument".to_string());
         }
 

          
@@ 324,7 370,7 @@ impl Operation for FnPoint {
     clone_impl!(FnPoint);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
         // check for 2 arguments
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Point requires exactly 2 arguments".to_string());
         }
 

          
@@ 356,7 402,7 @@ impl Operation for FnInradius {
     clone_impl!(FnInradius);
     fn call(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Inradius requires exactly 1 argument".to_string());
         }
 

          
@@ 381,7 427,7 @@ impl FnCircle {
     /// Case 1: create a circle from a point and a radius
     fn from_point_radius(&self, args: &[Value]) -> Result<Value, String> {
         // check for 2 arguments
-        if args.len() < 2 {
+        if args.len() != 2 {
             return Err("Circle requires exactly 2 arguments".to_string());
         }
 

          
@@ 407,7 453,7 @@ impl FnCircle {
     fn new(&self, args: &[Value]) -> Result<Value, String> {
         // check for no arguments
         if args.len() != 0 {
-            return Err("Circle requires exactly 2 arguments".to_string());
+            return Err("Circle requires no elements".to_string());
         }
 
         // try creating the circle

          
@@ 439,7 485,7 @@ impl FnTriangle {
     /// Case 1: create a triangle from three points
     fn from_points(&self, args: &[Value]) -> Result<Value, String> {
         // check for 3 arguments
-        if args.len() < 3 {
+        if args.len() != 3 {
             return Err("Triangle requires exactly 3 arguments".to_string());
         }
 

          
@@ 462,7 508,7 @@ impl FnTriangle {
     /// Case 2: create a triangle from an angle
     fn from_angle(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Triangle requires exactly 1 argument".to_string());
         }
 

          
@@ 487,7 533,7 @@ impl FnTriangle {
     /// Case 3 [ambiguous]: create a triangle from a circle
     fn from_circle(&self, args: &[Value]) -> Result<Value, String> {
         // check for 1 argument
-        if args.len() < 1 {
+        if args.len() != 1 {
             return Err("Triangle requires exactly 1 argument".to_string());
         }
 

          
M src/lang/types.rs +28 -0
@@ 269,6 269,7 @@ impl Triangle {
         Point { x, y }
     }
 
+    /// Return the orthocenter of the triangle
     pub fn orthocenter(&self) -> Point {
         // calculate the slopes of the sides
         let m1: f64 = (self.b.y - self.a.y) / (self.b.x - self.a.x);

          
@@ 292,4 293,31 @@ impl Triangle {
             y: (self.a.y + self.b.y + self.c.y) / 3.0,
         }
     }
+
+    /// Return the circumcenter of the triangle
+    pub fn circumcenter(&self) -> Point {
+        // calculate the midpoints of the sides
+        let m1 = Point {
+            x: (self.a.x + self.b.x) / 2.0,
+            y: (self.a.y + self.b.y) / 2.0,
+        };
+        let m2 = Point {
+            x: (self.b.x + self.c.x) / 2.0,
+            y: (self.b.y + self.c.y) / 2.0,
+        };
+
+        // calculate the slopes of the sides
+        let s1 = (self.b.y - self.a.y) / (self.b.x - self.a.x);
+        let s2 = (self.c.y - self.b.y) / (self.c.x - self.b.x);
+
+        // calculate the perpendicular slopes
+        let p1 = -1.0 / s1;
+        let p2 = -1.0 / s2;
+
+        // calculate the circumcenter
+        let x = (m2.y - m1.y + p1 * m1.x - p2 * m2.x) / (p1 - p2);
+        let y = p1 * (x - m1.x) + m1.y;
+
+        Point { x, y }
+    }
 }

          
M src/lexer.rs +10 -0
@@ 98,11 98,21 @@ fn match_fn(name: String) -> Function {
             args: Vec::new(),
             function: Box::new(functions::FnPoint),
         },
+        "midpoint" => Function {
+            name,
+            args: Vec::new(),
+            function: Box::new(functions::FnMidpoint),
+        },
         "lineseg" => Function {
             name,
             args: Vec::new(),
             function: Box::new(functions::FnLineseg),
         },
+        "circumcenter" => Function {
+            name,
+            args: Vec::new(),
+            function: Box::new(functions::FnCircumcenter),
+        },
         "incenter" => Function {
             name,
             args: Vec::new(),

          
M src/utils/geometry.rs +8 -0
@@ 1,5 1,13 @@ 
 use crate::lang::types::Point;
 
+/// Function that returns the midpoint between two points
+pub fn midpoint(first: Point, second: Point) -> Point {
+    Point {
+        x: (first.x + second.x) / 2.0,
+        y: (first.y + second.y) / 2.0,
+    }
+}
+
 /// Function that returns the distance between two points
 pub fn distance(first: Point, second: Point) -> f64 {
     ((first.x - second.x).powi(2) + (first.y - second.y).powi(2)).sqrt()