Skip to main content

kcl_lib/frontend/
api.rs

1//! An API for controlling the KCL interpreter from the frontend.
2
3#![allow(async_fn_in_trait)]
4
5use kcl_error::SourceRange;
6use kittycad_modeling_cmds::units::UnitLength;
7use serde::{Deserialize, Serialize};
8
9pub use crate::ExecutorSettings as Settings;
10use crate::{ExecOutcome, engine::PlaneName, execution::ArtifactId, pretty::NumericSuffix};
11
12pub trait LifecycleApi {
13    async fn open_project(&self, project: ProjectId, files: Vec<File>, open_file: FileId) -> Result<()>;
14    async fn add_file(&self, project: ProjectId, file: File) -> Result<()>;
15    async fn remove_file(&self, project: ProjectId, file: FileId) -> Result<()>;
16    // File changed on disk, etc. outside of the editor or applying undo, restore, etc.
17    async fn update_file(&self, project: ProjectId, file: FileId, text: String) -> Result<()>;
18    async fn switch_file(&self, project: ProjectId, file: FileId) -> Result<()>;
19    async fn refresh(&self, project: ProjectId) -> Result<()>;
20}
21
22#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
23#[ts(export, export_to = "FrontendApi.ts")]
24pub struct SceneGraph {
25    pub project: ProjectId,
26    pub file: FileId,
27    pub version: Version,
28
29    pub objects: Vec<Object>,
30    pub settings: Settings,
31    pub sketch_mode: Option<ObjectId>,
32}
33
34impl SceneGraph {
35    pub fn empty(project: ProjectId, file: FileId, version: Version) -> Self {
36        SceneGraph {
37            project,
38            file,
39            version,
40            objects: Vec::new(),
41            settings: Default::default(),
42            sketch_mode: None,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Serialize, ts_rs::TS)]
48#[ts(export, export_to = "FrontendApi.ts")]
49pub struct SceneGraphDelta {
50    pub new_graph: SceneGraph,
51    pub new_objects: Vec<ObjectId>,
52    pub invalidates_ids: bool,
53    pub exec_outcome: ExecOutcome,
54}
55
56impl SceneGraphDelta {
57    pub fn new(
58        new_graph: SceneGraph,
59        new_objects: Vec<ObjectId>,
60        invalidates_ids: bool,
61        exec_outcome: ExecOutcome,
62    ) -> Self {
63        SceneGraphDelta {
64            new_graph,
65            new_objects,
66            invalidates_ids,
67            exec_outcome,
68        }
69    }
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
73#[ts(export, export_to = "FrontendApi.ts")]
74pub struct SourceDelta {
75    pub text: String,
76}
77
78#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
79#[ts(export, export_to = "FrontendApi.ts", rename = "ApiObjectId")]
80pub struct ObjectId(pub usize);
81
82impl ObjectId {
83    pub fn predecessor(self) -> Option<Self> {
84        self.0.checked_sub(1).map(ObjectId)
85    }
86}
87
88#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ts_rs::TS)]
89#[ts(export, export_to = "FrontendApi.ts", rename = "ApiVersion")]
90pub struct Version(pub usize);
91
92#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
93#[ts(export, export_to = "FrontendApi.ts", rename = "ApiProjectId")]
94pub struct ProjectId(pub usize);
95
96#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
97#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFileId")]
98pub struct FileId(pub usize);
99
100#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
101#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFile")]
102pub struct File {
103    pub id: FileId,
104    pub path: String,
105    pub text: String,
106}
107
108#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
109#[ts(export, export_to = "FrontendApi.ts", rename = "ApiObject")]
110pub struct Object {
111    pub id: ObjectId,
112    pub kind: ObjectKind,
113    pub label: String,
114    pub comments: String,
115    pub artifact_id: ArtifactId,
116    pub source: SourceRef,
117}
118
119impl Object {
120    pub fn placeholder(id: ObjectId, range: SourceRange) -> Self {
121        Object {
122            id,
123            kind: ObjectKind::Nil,
124            label: Default::default(),
125            comments: Default::default(),
126            artifact_id: ArtifactId::placeholder(),
127            source: SourceRef::Simple { range },
128        }
129    }
130}
131
132#[allow(clippy::large_enum_variant)]
133#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
134#[ts(export, export_to = "FrontendApi.ts", rename = "ApiObjectKind")]
135#[serde(tag = "type")]
136pub enum ObjectKind {
137    /// A placeholder for an object that will be solved and replaced later.
138    Nil,
139    Plane(Plane),
140    Face(Face),
141    Sketch(crate::frontend::sketch::Sketch),
142    // These need to be named since the nested types are also enums. ts-rs needs
143    // a place to put the type tag.
144    Segment {
145        segment: crate::frontend::sketch::Segment,
146    },
147    Constraint {
148        constraint: crate::frontend::sketch::Constraint,
149    },
150}
151
152#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
153#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPlane")]
154#[serde(rename_all = "camelCase")]
155pub enum Plane {
156    Object(ObjectId),
157    Default(PlaneName),
158}
159
160#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
161#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFace")]
162#[serde(rename_all = "camelCase")]
163pub struct Face {
164    pub id: ObjectId,
165}
166
167#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
168#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSourceRef")]
169#[serde(tag = "type")]
170pub enum SourceRef {
171    Simple { range: SourceRange },
172    BackTrace { ranges: Vec<SourceRange> },
173}
174
175impl From<SourceRange> for SourceRef {
176    fn from(value: SourceRange) -> Self {
177        Self::Simple { range: value }
178    }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
182#[ts(export, export_to = "FrontendApi.ts")]
183pub struct Number {
184    pub value: f64,
185    pub units: NumericSuffix,
186}
187
188impl TryFrom<crate::std::args::TyF64> for Number {
189    type Error = crate::execution::types::NumericSuffixTypeConvertError;
190
191    fn try_from(value: crate::std::args::TyF64) -> std::result::Result<Self, Self::Error> {
192        Ok(Number {
193            value: value.n,
194            units: value.ty.try_into()?,
195        })
196    }
197}
198
199impl Number {
200    pub fn round(&self, digits: u8) -> Self {
201        let factor = 10f64.powi(digits as i32);
202        let rounded_value = (self.value * factor).round() / factor;
203        // Don't return negative zero.
204        let value = if rounded_value == -0.0 { 0.0 } else { rounded_value };
205        Number {
206            value,
207            units: self.units,
208        }
209    }
210}
211
212impl From<(f64, UnitLength)> for Number {
213    fn from((value, units): (f64, UnitLength)) -> Self {
214        // Direct conversion from UnitLength to NumericSuffix (never panics)
215        // The From<UnitLength> for NumericSuffix impl is in execution::types
216        let units_suffix = NumericSuffix::from(units);
217        Number {
218            value,
219            units: units_suffix,
220        }
221    }
222}
223
224#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
225#[ts(export, export_to = "FrontendApi.ts")]
226#[serde(tag = "type")]
227pub enum Expr {
228    Number(Number),
229    Var(Number),
230    Variable(String),
231}
232
233#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
234#[ts(export, export_to = "FrontendApi.ts")]
235pub struct Error {
236    pub msg: String,
237}
238
239impl Error {
240    pub fn file_id_in_use(id: FileId, path: &str) -> Self {
241        Error {
242            msg: format!("File ID already in use: {id:?}, currently used for `{path}`"),
243        }
244    }
245
246    pub fn bad_project(found: ProjectId, expected: Option<ProjectId>) -> Self {
247        let msg = match expected {
248            Some(expected) => format!("Project ID mismatch found: {found:?}, expected: {expected:?}"),
249            None => format!("No open project, found: {found:?}"),
250        };
251        Error { msg }
252    }
253
254    pub fn bad_version(found: Version, expected: Version) -> Self {
255        Error {
256            msg: format!("Version mismatch found: {found:?}, expected: {expected:?}"),
257        }
258    }
259
260    pub fn bad_file(found: FileId, expected: Option<FileId>) -> Self {
261        let msg = match expected {
262            Some(expected) => format!("File ID mismatch found: {found:?}, expected: {expected:?}"),
263            None => format!("File ID not found: {found:?}"),
264        };
265        Error { msg }
266    }
267
268    pub fn serialize(e: impl serde::ser::Error) -> Self {
269        Error {
270            msg: format!(
271                "Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
272            ),
273        }
274    }
275
276    pub fn deserialize(name: &str, e: impl serde::de::Error) -> Self {
277        Error {
278            msg: format!(
279                "Could not deserialize argument `{name}`. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
280            ),
281        }
282    }
283}
284
285pub type Result<T> = std::result::Result<T, Error>;