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