microcad-builtin 0.5.1

Provides the built-in functionalities available in the µcad language.
Documentation
// Copyright © 2024-2026 The µcad authors <info@microcad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

use microcad_builtin_proc_macros::builtin_mod;

use cgmath::{InnerSpace, SquareMatrix};
use microcad_core::{Mat3, Vec3};

pub(crate) fn orient_z_to(target: Vec3) -> Mat3 {
    let z_axis = Vec3::unit_z();
    let target = target.normalize();

    // Handle edge case where target is already Z
    if (target - z_axis).magnitude2() < 1e-6 {
        return Mat3::identity();
    }

    // Handle 180-degree rotation (target is -Z)
    if (target + z_axis).magnitude2() < 1e-6 {
        // Rotate 180° around any axis perpendicular to Z.
        // For stability, pick X if possible, otherwise Y.
        let perp_axis = if z_axis.cross(Vec3::unit_x()).magnitude2() > 1e-6 {
            Vec3::unit_x()
        } else {
            Vec3::unit_y()
        };
        return Mat3::from_axis_angle(perp_axis, cgmath::Rad(std::f64::consts::PI));
    }

    // Normal case
    let rotation_axis = z_axis.cross(target).normalize();
    let dot = z_axis.dot(target).clamp(-1.0, 1.0); // avoid NaNs
    let angle = cgmath::Rad(dot.acos());

    Mat3::from_axis_angle(rotation_axis, angle)
}

/// µcad math built-in module.
#[builtin_mod]
#[allow(clippy::module_inception)]
pub mod math {
    use cgmath::SquareMatrix;
    use microcad_builtin_proc_macros::builtin_fn;
    use microcad_core::{Integer, Mat3, Scalar, Vec3};
    use microcad_lang::{eval::*, parameter, symbol::Symbol, ty::*, value::*};
    use microcad_lang_base::PushDiag;

    /// Pi.
    pub const PI: Scalar = std::f64::consts::PI;

    /// X unit vector.
    pub const X: Vec3 = Vec3::new(1.0, 0.0, 0.0);

    /// Y unit vector.
    pub const Y: Vec3 = Vec3::new(0.0, 1.0, 0.0);

    /// Z unit vector.
    pub const Z: Vec3 = Vec3::new(0.0, 0.0, 1.0);

    /// Absolute value abs(x)
    #[builtin_fn(x)]
    pub fn abs() -> Symbol {
        |_params, args, ctx| {
            let (_, arg) = args.get_single()?;
            Ok(match &arg.value {
                Value::Integer(i) => Value::Integer(i.abs()),
                Value::Quantity(q) => Value::Quantity(q.clone().map(|v| v.abs())),
                value => {
                    ctx.error(
                        arg,
                        EvalError::BuiltinError(format!("Cannot calculate abs({value})")),
                    )?;
                    Value::None
                }
            })
        }
    }

    /// Calculate the square root `sqrt(x)`.
    #[builtin_fn(x)]
    pub fn sqrt() -> Symbol {
        |_params, args, ctx| {
            let (_, arg) = args.get_single()?;
            Ok(match &arg.value {
                Value::Integer(i) => (*i as Scalar).sqrt().into(),
                Value::Quantity(q) => Value::Quantity(q.clone().map(|v| v.sqrt())),
                value => {
                    ctx.error(
                        arg,
                        EvalError::BuiltinError(format!("Cannot calculate sqrt({value})")),
                    )?;
                    Value::None
                }
            })
        }
    }

    /// Cast some Quantity into an Integer.
    #[builtin_fn(x)]
    pub fn int() -> Symbol {
        |_params, args, ctx| {
            let (_, arg) = args.get_single()?;
            Ok(match &arg.value {
                Value::Integer(i) => Value::Integer(*i),
                Value::Quantity(q) => Value::Integer(q.value.floor() as Integer),
                value => {
                    ctx.error(
                        arg,
                        EvalError::BuiltinError(format!("Cannot calculate int({value})")),
                    )?;
                    Value::None
                }
            })
        }
    }

    /// Implementation for a builtin trigonometric function.
    fn trigonometric(
        args: &ArgumentValueList,
        ctx: &mut EvalContext,
        f: impl FnOnce(f64) -> f64,
    ) -> EvalResult<Value> {
        let (_, arg) = args.get_single()?;
        Ok(match &arg.value {
            Value::Integer(i) => Value::Quantity(Quantity::new(f(*i as f64), QuantityType::Scalar)),
            Value::Quantity(Quantity {
                value,
                quantity_type: QuantityType::Angle,
                ..
            })
            | Value::Quantity(Quantity {
                value,
                quantity_type: QuantityType::Scalar,
                ..
            }) => Value::Quantity(Quantity::new(f(*value), QuantityType::Scalar)),
            value => {
                ctx.error(
                    arg,
                    EvalError::BuiltinError(format!(
                        "Value must be a scalar, angle or integer. Got {value}"
                    )),
                )?;
                Value::None
            }
        })
    }

    /// Calculate cos(x).
    #[builtin_fn(x)]
    pub fn cos() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.cos())
    }

    /// Calculate sin(x).
    #[builtin_fn(x)]
    pub fn sin() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.sin())
    }

    /// Calculate tan(x).
    #[builtin_fn(x)]
    pub fn tan() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.tan())
    }

    /// Calculate acos(x).
    #[builtin_fn(x)]
    pub fn acos() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.acos())
    }

    /// Calculate asin(x).
    #[builtin_fn(x)]
    pub fn asin() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.asin())
    }

    /// Calculate atan(x).
    #[builtin_fn(x)]
    pub fn atan() -> Symbol {
        |_params, args, ctx| trigonometric(args, ctx, |v| v.atan())
    }

    /// Helper function to get an angle from a field in an argument list.
    ///
    /// Returns `None` if the argument is not an angle.
    fn get_angle(args: &Tuple, axis: &str) -> Option<cgmath::Rad<f64>> {
        match args.get_value(axis).expect("Argument expected") {
            Value::Quantity(Quantity {
                value,
                quantity_type: QuantityType::Angle,
                ..
            }) => Some(cgmath::Rad::<f64>(*value)),
            _ => None,
        }
    }

    /// Helper function to return rotation X,Y,Z rotation matrices from an [`Tuple`].
    fn rotation_matrices_xyz(args: &Tuple) -> (Mat3, Mat3, Mat3) {
        match (
            get_angle(args, "x"),
            get_angle(args, "y"),
            get_angle(args, "z"),
        ) {
            (Some(angle_x), Some(angle_y), Some(angle_z)) => (
                Mat3::from_angle_x(angle_x),
                Mat3::from_angle_y(angle_y),
                Mat3::from_angle_z(angle_z),
            ),
            _ => (Mat3::identity(), Mat3::identity(), Mat3::identity()),
        }
    }

    /// Rotate a vector around an axis.
    pub fn rotate_around_axis() -> Symbol {
        Symbol::new_builtin_fn(
            "rotate_around_axis",
            [
                parameter!(angle: Angle),
                parameter!(x: Scalar),
                parameter!(y: Scalar),
                parameter!(z: Scalar),
            ]
            .into_iter(),
            &|params, args, ctx| match ArgumentMatch::find_match(args, params) {
                Ok(ref args) => Ok(match get_angle(args, "angle") {
                    Some(angle) => {
                        let axis = Vec3::new(args.get("x"), args.get("y"), args.get("z"));
                        let matrix = Mat3::from_axis_angle(axis, angle);
                        Value::Matrix(Box::new(Matrix::Matrix3(matrix)))
                    }
                    None => Value::None,
                }),
                Err(err) => {
                    ctx.error(args, err)?;
                    Ok(Value::None)
                }
            },
            None,
        )
    }

    /// Rotate around X, Y, Z (in that order).
    #[builtin_fn(x: Angle, y: Angle, z: Angle)]
    pub fn rotate_xyz() -> Symbol {
        |params, args, ctx| match ArgumentMatch::find_match(args, params) {
            Ok(args) => {
                let (x_matrix, y_matrix, z_matrix) = rotation_matrices_xyz(&args);
                Ok(Value::Matrix(Box::new(Matrix::Matrix3(
                    x_matrix * y_matrix * z_matrix,
                ))))
            }
            Err(err) => {
                ctx.error(args, err)?;
                Ok(Value::None)
            }
        }
    }

    /// Rotate around Z, Y, X (in that order).
    #[builtin_fn(x: Angle, y: Angle, z: Angle)]
    pub fn rotate_zyx() -> Symbol {
        |params, args, ctx| match ArgumentMatch::find_match(args, params) {
            Ok(args) => {
                let (x_matrix, y_matrix, z_matrix) = rotation_matrices_xyz(&args);
                Ok(Value::Matrix(Box::new(Matrix::Matrix3(
                    z_matrix * y_matrix * x_matrix,
                ))))
            }
            Err(err) => {
                ctx.error(args, err)?;
                Ok(Value::None)
            }
        }
    }
}