kcl-lib 0.2.192

KittyCAD Language implementation and tools
Documentation
//! An API for controlling the KCL interpreter from the frontend.

#![allow(async_fn_in_trait)]

use kcl_api::Number;
use kcl_api::Object;
use kcl_api::ObjectId;
use kcl_api::ObjectKind;
use serde::Deserialize;
use serde::Serialize;

use crate::ExecOutcome;
pub use crate::ExecutorSettings as Settings;

pub trait LifecycleApi {
    async fn open_project(&self, project: ProjectId, files: Vec<File>, open_file: FileId) -> Result<()>;
    async fn get_project(&self, project: ProjectId) -> Result<Vec<File>>;
    async fn add_file(&self, project: ProjectId, file: File) -> Result<()>;
    async fn get_file(&self, project: ProjectId, file: FileId) -> Result<File>;
    async fn remove_file(&self, project: ProjectId, file: FileId) -> Result<()>;
    // File changed on disk, etc. outside of the editor or applying undo, restore, etc.
    async fn update_file(&self, project: ProjectId, file: FileId, text: String) -> Result<()>;
    async fn switch_file(&self, project: ProjectId, file: FileId) -> Result<()>;
    async fn refresh(&self, project: ProjectId) -> Result<()>;
}

#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
pub struct SceneGraph {
    pub project: ProjectId,
    pub file: FileId,
    pub version: Version,

    pub objects: Vec<Object>,
    pub settings: Settings,
    pub sketch_mode: Option<ObjectId>,
}

impl SceneGraph {
    pub fn empty(project: ProjectId, file: FileId, version: Version) -> Self {
        SceneGraph {
            project,
            file,
            version,
            objects: Vec::new(),
            settings: Default::default(),
            sketch_mode: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
pub struct SceneGraphDelta {
    pub new_graph: SceneGraph,
    pub new_objects: Vec<ObjectId>,
    pub invalidates_ids: bool,
    pub exec_outcome: ExecOutcome,
}

impl SceneGraphDelta {
    pub fn new(
        new_graph: SceneGraph,
        new_objects: Vec<ObjectId>,
        invalidates_ids: bool,
        exec_outcome: ExecOutcome,
    ) -> Self {
        SceneGraphDelta {
            new_graph,
            new_objects,
            invalidates_ids,
            exec_outcome,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
pub struct SourceDelta {
    pub text: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ts_rs::TS)]
pub struct SketchCheckpointId(u64);

impl SketchCheckpointId {
    pub(crate) fn new(n: u64) -> Self {
        Self(n)
    }
}

#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(rename_all = "camelCase")]
pub struct SketchMutationOutcome {
    pub source_delta: SourceDelta,
    pub scene_graph_delta: SceneGraphDelta,
    pub checkpoint_id: Option<SketchCheckpointId>,
}

#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(rename_all = "camelCase")]
pub struct NewSketchOutcome {
    pub source_delta: SourceDelta,
    pub scene_graph_delta: SceneGraphDelta,
    pub sketch_id: ObjectId,
    pub checkpoint_id: Option<SketchCheckpointId>,
}

#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(rename_all = "camelCase")]
pub struct EditSketchOutcome {
    pub scene_graph_delta: SceneGraphDelta,
    pub checkpoint_id: Option<SketchCheckpointId>,
}

#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(rename_all = "camelCase")]
pub struct RestoreSketchCheckpointOutcome {
    pub source_delta: SourceDelta,
    pub scene_graph_delta: SceneGraphDelta,
}

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts", rename = "ApiVersion")]
pub struct Version(pub usize);

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts", rename = "ApiProjectId")]
pub struct ProjectId(pub usize);

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFileId")]
pub struct FileId(pub usize);

#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFile")]
pub struct File {
    pub id: FileId,
    pub path: String,
    pub text: String,
}

/// Runtime helpers for [`ObjectKind`].
pub trait ObjectKindExt {
    /// What kind of object is this (point, line, arc, etc)
    /// Suitable for use in user-facing messages.
    fn human_friendly_kind_with_article(&self) -> &'static str;
}

impl ObjectKindExt for ObjectKind {
    fn human_friendly_kind_with_article(&self) -> &'static str {
        match self {
            Self::Nil => "a Nil",
            Self::Plane(..) => "a Plane",
            Self::Face(..) => "a Face",
            Self::Wall(..) => "a Wall",
            Self::Cap(..) => "a Cap",
            Self::Sketch(..) => "a Sketch",
            Self::Segment { .. } => "a Segment",
            Self::Constraint { .. } => "a Constraint",
        }
    }
}

impl TryFrom<crate::std::args::TyF64> for Number {
    type Error = kcl_api::NumericSuffixTypeConvertError;

    fn try_from(value: crate::std::args::TyF64) -> std::result::Result<Self, Self::Error> {
        Ok(Number {
            value: value.n,
            units: value.ty.try_into()?,
        })
    }
}

/// Runtime helpers for [`Number`].
pub trait NumberExt {
    fn round(&self, digits: u8) -> Self;
}

impl NumberExt for Number {
    fn round(&self, digits: u8) -> Self {
        let factor = 10f64.powi(digits as i32);
        let rounded_value = (self.value * factor).round() / factor;
        // Don't return negative zero.
        let value = if rounded_value == -0.0 { 0.0 } else { rounded_value };
        Number {
            value,
            units: self.units,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
pub struct Error {
    pub msg: String,
}

impl Error {
    pub fn file_id_in_use(id: FileId, path: &str) -> Self {
        Error {
            msg: format!("File ID already in use: {id:?}, currently used for `{path}`"),
        }
    }

    pub fn file_id_not_found(project_id: ProjectId, file_id: FileId) -> Self {
        Error {
            msg: format!("File ID not found in project: {file_id:?}, project: {project_id:?}"),
        }
    }

    pub fn bad_project(found: ProjectId, expected: Option<ProjectId>) -> Self {
        let msg = match expected {
            Some(expected) => format!("Project ID mismatch found: {found:?}, expected: {expected:?}"),
            None => format!("No open project, found: {found:?}"),
        };
        Error { msg }
    }

    pub fn bad_version(found: Version, expected: Version) -> Self {
        Error {
            msg: format!("Version mismatch found: {found:?}, expected: {expected:?}"),
        }
    }

    pub fn bad_file(found: FileId, expected: Option<FileId>) -> Self {
        let msg = match expected {
            Some(expected) => format!("File ID mismatch found: {found:?}, expected: {expected:?}"),
            None => format!("File ID not found: {found:?}"),
        };
        Error { msg }
    }

    pub fn serialize(e: impl serde::ser::Error) -> Self {
        Error {
            msg: format!(
                "Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
            ),
        }
    }

    pub fn deserialize(name: &str, e: impl serde::de::Error) -> Self {
        Error {
            msg: format!(
                "Could not deserialize argument `{name}`. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
            ),
        }
    }
}

pub type Result<T> = std::result::Result<T, Error>;