kcl-lib 0.2.147

KittyCAD Language implementation and tools
Documentation
//! Standard library helices.

use anyhow::Result;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kcmc::length_unit::LengthUnit;
use kcmc::shared::Angle;
use kittycad_modeling_cmds::shared::Point3d;
use kittycad_modeling_cmds::{self as kcmc};

use super::args::TyF64;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::ExecState;
use crate::execution::Helix as HelixValue;
use crate::execution::KclValue;
use crate::execution::ModelingCmdMeta;
use crate::execution::Solid;
use crate::execution::types::PrimitiveType;
use crate::execution::types::RuntimeType;
use crate::std::Args;
use crate::std::axis_or_reference::Axis3dOrEdgeReference;

/// Create a helix.
pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
    let angle_start: TyF64 = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
    let revolutions: TyF64 = args.get_kw_arg("revolutions", &RuntimeType::count(), exec_state)?;
    let ccw = args.get_kw_arg_opt("ccw", &RuntimeType::bool(), exec_state)?;
    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
    let axis: Option<Axis3dOrEdgeReference> = args.get_kw_arg_opt(
        "axis",
        &RuntimeType::Union(vec![
            RuntimeType::Primitive(PrimitiveType::Edge),
            RuntimeType::Primitive(PrimitiveType::Axis3d),
            RuntimeType::segment(),
        ]),
        exec_state,
    )?;
    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
    let cylinder = args.get_kw_arg_opt("cylinder", &RuntimeType::solid(), exec_state)?;

    // Make sure we have a radius if we don't have a cylinder.
    if radius.is_none() && cylinder.is_none() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Radius is required when creating a helix without a cylinder.".to_string(),
            vec![args.source_range],
        )));
    }

    // Make sure we don't have a radius if we have a cylinder.
    if radius.is_some() && cylinder.is_some() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Radius is not allowed when creating a helix with a cylinder.".to_string(),
            vec![args.source_range],
        )));
    }

    // Make sure we have an axis if we don't have a cylinder.
    if axis.is_none() && cylinder.is_none() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Axis is required when creating a helix without a cylinder.".to_string(),
            vec![args.source_range],
        )));
    }

    // Make sure we don't have an axis if we have a cylinder.
    if axis.is_some() && cylinder.is_some() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Axis is not allowed when creating a helix with a cylinder.".to_string(),
            vec![args.source_range],
        )));
    }

    // Make sure we have a radius if we have an axis.
    if radius.is_none() && axis.is_some() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Radius is required when creating a helix around an axis.".to_string(),
            vec![args.source_range],
        )));
    }

    // Make sure we have an axis if we have a radius.
    if axis.is_none() && radius.is_some() {
        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
            "Axis is required when creating a helix around an axis.".to_string(),
            vec![args.source_range],
        )));
    }

    let value = inner_helix(
        revolutions.n,
        angle_start.n,
        ccw,
        radius,
        axis,
        length,
        cylinder,
        exec_state,
        args,
    )
    .await?;
    Ok(KclValue::Helix { value })
}

#[allow(clippy::too_many_arguments)]
async fn inner_helix(
    revolutions: f64,
    angle_start: f64,
    ccw: Option<bool>,
    radius: Option<TyF64>,
    axis: Option<Axis3dOrEdgeReference>,
    length: Option<TyF64>,
    cylinder: Option<Solid>,
    exec_state: &mut ExecState,
    args: Args,
) -> Result<Box<HelixValue>, KclError> {
    let id = exec_state.next_uuid();

    let helix_result = Box::new(HelixValue {
        value: id,
        artifact_id: id.into(),
        revolutions,
        angle_start,
        cylinder_id: cylinder.as_ref().map(|c| c.id),
        ccw: ccw.unwrap_or(false),
        units: exec_state.length_unit(),
        meta: vec![args.source_range.into()],
    });

    if args.ctx.no_engine_commands().await {
        return Ok(helix_result);
    }

    if let Some(cylinder) = cylinder {
        let cmd = if let Some(length) = length {
            mcmd::EntityMakeHelix::builder()
                .cylinder_id(cylinder.id)
                .is_clockwise(!helix_result.ccw)
                .revolutions(revolutions)
                .start_angle(Angle::from_degrees(angle_start))
                .length(LengthUnit(length.to_mm()))
                .build()
        } else {
            mcmd::EntityMakeHelix::builder()
                .cylinder_id(cylinder.id)
                .is_clockwise(!helix_result.ccw)
                .revolutions(revolutions)
                .start_angle(Angle::from_degrees(angle_start))
                .build()
        };
        exec_state
            .batch_modeling_cmd(
                ModelingCmdMeta::from_args_id(exec_state, &args, id),
                ModelingCmd::from(cmd),
            )
            .await?;
    } else if let (Some(axis), Some(radius)) = (axis, radius) {
        match axis {
            Axis3dOrEdgeReference::Axis { direction, origin } => {
                // Make sure they gave us a length.
                let Some(length) = length else {
                    return Err(KclError::new_semantic(KclErrorDetails::new(
                        "Length is required when creating a helix around an axis.".to_owned(),
                        vec![args.source_range],
                    )));
                };

                exec_state
                    .batch_modeling_cmd(
                        ModelingCmdMeta::from_args_id(exec_state, &args, id),
                        ModelingCmd::from(
                            mcmd::EntityMakeHelixFromParams::builder()
                                .radius(LengthUnit(radius.to_mm()))
                                .is_clockwise(!helix_result.ccw)
                                .length(LengthUnit(length.to_mm()))
                                .revolutions(revolutions)
                                .start_angle(Angle::from_degrees(angle_start))
                                .axis(Point3d {
                                    x: direction[0].to_mm(),
                                    y: direction[1].to_mm(),
                                    z: direction[2].to_mm(),
                                })
                                .center(Point3d {
                                    x: LengthUnit(origin[0].to_mm()),
                                    y: LengthUnit(origin[1].to_mm()),
                                    z: LengthUnit(origin[2].to_mm()),
                                })
                                .build(),
                        ),
                    )
                    .await?;
            }
            Axis3dOrEdgeReference::Edge(edge) => {
                let edge_id = edge.get_engine_id(exec_state, &args)?;

                let cmd = if let Some(length) = length {
                    mcmd::EntityMakeHelixFromEdge::builder()
                        .radius(LengthUnit(radius.to_mm()))
                        .is_clockwise(!helix_result.ccw)
                        .revolutions(revolutions)
                        .start_angle(Angle::from_degrees(angle_start))
                        .edge_id(edge_id)
                        .length(LengthUnit(length.to_mm()))
                        .build()
                } else {
                    mcmd::EntityMakeHelixFromEdge::builder()
                        .radius(LengthUnit(radius.to_mm()))
                        .is_clockwise(!helix_result.ccw)
                        .revolutions(revolutions)
                        .start_angle(Angle::from_degrees(angle_start))
                        .edge_id(edge_id)
                        .build()
                };
                exec_state
                    .batch_modeling_cmd(
                        ModelingCmdMeta::from_args_id(exec_state, &args, id),
                        ModelingCmd::from(cmd),
                    )
                    .await?;
            }
        };
    }

    Ok(helix_result)
}