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