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
205impl ObjectKind {
206    /// What kind of object is this (point, line, arc, etc)
207    /// Suitable for use in user-facing messages.
208    pub fn human_friendly_kind_with_article(&self) -> &'static str {
209        match self {
210            Self::Nil => "a Nil",
211            Self::Plane(..) => "a Plane",
212            Self::Face(..) => "a Face",
213            Self::Wall(..) => "a Wall",
214            Self::Cap(..) => "a Cap",
215            Self::Sketch(..) => "a Sketch",
216            Self::Segment { .. } => "a Segment",
217            Self::Constraint { .. } => "a Constraint",
218        }
219    }
220}
221
222#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
223#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPlane")]
224#[serde(rename_all = "camelCase")]
225pub enum Plane {
226    Object(ObjectId),
227    Default(PlaneName),
228}
229
230#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
231#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFace")]
232#[serde(rename_all = "camelCase")]
233pub struct Face {
234    pub id: ObjectId,
235}
236
237#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
238#[ts(export, export_to = "FrontendApi.ts", rename = "ApiWall")]
239#[serde(rename_all = "camelCase")]
240pub struct Wall {
241    pub id: ObjectId,
242}
243
244#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
245#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCap")]
246#[serde(rename_all = "camelCase")]
247pub struct Cap {
248    pub id: ObjectId,
249    pub kind: CapKind,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ts_rs::TS)]
253#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCapKind")]
254#[serde(rename_all = "camelCase")]
255pub enum CapKind {
256    Start,
257    End,
258}
259
260#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
261#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSourceRef")]
262#[serde(tag = "type")]
263pub enum SourceRef {
264    Simple {
265        range: SourceRange,
266        node_path: Option<NodePath>,
267    },
268    BackTrace {
269        ranges: Vec<(SourceRange, Option<NodePath>)>,
270    },
271}
272
273impl From<SourceRange> for SourceRef {
274    fn from(value: SourceRange) -> Self {
275        Self::Simple {
276            range: value,
277            node_path: None,
278        }
279    }
280}
281
282impl SourceRef {
283    pub fn new(range: SourceRange, node_path: Option<NodePath>) -> Self {
284        Self::Simple { range, node_path }
285    }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
289#[ts(export, export_to = "FrontendApi.ts")]
290pub struct Number {
291    pub value: f64,
292    pub units: NumericSuffix,
293}
294
295impl TryFrom<crate::std::args::TyF64> for Number {
296    type Error = crate::execution::types::NumericSuffixTypeConvertError;
297
298    fn try_from(value: crate::std::args::TyF64) -> std::result::Result<Self, Self::Error> {
299        Ok(Number {
300            value: value.n,
301            units: value.ty.try_into()?,
302        })
303    }
304}
305
306impl Number {
307    pub fn round(&self, digits: u8) -> Self {
308        let factor = 10f64.powi(digits as i32);
309        let rounded_value = (self.value * factor).round() / factor;
310        // Don't return negative zero.
311        let value = if rounded_value == -0.0 { 0.0 } else { rounded_value };
312        Number {
313            value,
314            units: self.units,
315        }
316    }
317}
318
319impl From<(f64, UnitLength)> for Number {
320    fn from((value, units): (f64, UnitLength)) -> Self {
321        // Direct conversion from UnitLength to NumericSuffix (never panics)
322        // The From<UnitLength> for NumericSuffix impl is in execution::types
323        let units_suffix = NumericSuffix::from(units);
324        Number {
325            value,
326            units: units_suffix,
327        }
328    }
329}
330
331#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
332#[ts(export, export_to = "FrontendApi.ts")]
333#[serde(tag = "type")]
334pub enum Expr {
335    Number(Number),
336    Var(Number),
337    Variable(String),
338}
339
340#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
341#[ts(export, export_to = "FrontendApi.ts")]
342pub struct Error {
343    pub msg: String,
344}
345
346impl Error {
347    pub fn file_id_in_use(id: FileId, path: &str) -> Self {
348        Error {
349            msg: format!("File ID already in use: {id:?}, currently used for `{path}`"),
350        }
351    }
352
353    pub fn file_id_not_found(project_id: ProjectId, file_id: FileId) -> Self {
354        Error {
355            msg: format!("File ID not found in project: {file_id:?}, project: {project_id:?}"),
356        }
357    }
358
359    pub fn bad_project(found: ProjectId, expected: Option<ProjectId>) -> Self {
360        let msg = match expected {
361            Some(expected) => format!("Project ID mismatch found: {found:?}, expected: {expected:?}"),
362            None => format!("No open project, found: {found:?}"),
363        };
364        Error { msg }
365    }
366
367    pub fn bad_version(found: Version, expected: Version) -> Self {
368        Error {
369            msg: format!("Version mismatch found: {found:?}, expected: {expected:?}"),
370        }
371    }
372
373    pub fn bad_file(found: FileId, expected: Option<FileId>) -> Self {
374        let msg = match expected {
375            Some(expected) => format!("File ID mismatch found: {found:?}, expected: {expected:?}"),
376            None => format!("File ID not found: {found:?}"),
377        };
378        Error { msg }
379    }
380
381    pub fn serialize(e: impl serde::ser::Error) -> Self {
382        Error {
383            msg: format!(
384                "Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
385            ),
386        }
387    }
388
389    pub fn deserialize(name: &str, e: impl serde::de::Error) -> Self {
390        Error {
391            msg: format!(
392                "Could not deserialize argument `{name}`. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
393            ),
394        }
395    }
396}
397
398pub type Result<T> = std::result::Result<T, Error>;