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_api::Number;
6use kcl_api::Object;
7use kcl_api::ObjectId;
8use kcl_api::ObjectKind;
9use serde::Deserialize;
10use serde::Serialize;
11
12use crate::ExecOutcome;
13pub use crate::ExecutorSettings as Settings;
14
15pub trait LifecycleApi {
16    async fn open_project(&self, project: ProjectId, files: Vec<File>, open_file: FileId) -> Result<()>;
17    async fn get_project(&self, project: ProjectId) -> Result<Vec<File>>;
18    async fn add_file(&self, project: ProjectId, file: File) -> Result<()>;
19    async fn get_file(&self, project: ProjectId, file: FileId) -> Result<File>;
20    async fn remove_file(&self, project: ProjectId, file: FileId) -> Result<()>;
21    // File changed on disk, etc. outside of the editor or applying undo, restore, etc.
22    async fn update_file(&self, project: ProjectId, file: FileId, text: String) -> Result<()>;
23    async fn switch_file(&self, project: ProjectId, file: FileId) -> Result<()>;
24    async fn refresh(&self, project: ProjectId) -> Result<()>;
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
28#[ts(export, export_to = "FrontendApi.ts")]
29pub struct SceneGraph {
30    pub project: ProjectId,
31    pub file: FileId,
32    pub version: Version,
33
34    pub objects: Vec<Object>,
35    pub settings: Settings,
36    pub sketch_mode: Option<ObjectId>,
37}
38
39impl SceneGraph {
40    pub fn empty(project: ProjectId, file: FileId, version: Version) -> Self {
41        SceneGraph {
42            project,
43            file,
44            version,
45            objects: Vec::new(),
46            settings: Default::default(),
47            sketch_mode: None,
48        }
49    }
50}
51
52#[derive(Debug, Clone, Serialize, ts_rs::TS)]
53#[ts(export, export_to = "FrontendApi.ts")]
54pub struct SceneGraphDelta {
55    pub new_graph: SceneGraph,
56    pub new_objects: Vec<ObjectId>,
57    pub invalidates_ids: bool,
58    pub exec_outcome: ExecOutcome,
59}
60
61impl SceneGraphDelta {
62    pub fn new(
63        new_graph: SceneGraph,
64        new_objects: Vec<ObjectId>,
65        invalidates_ids: bool,
66        exec_outcome: ExecOutcome,
67    ) -> Self {
68        SceneGraphDelta {
69            new_graph,
70            new_objects,
71            invalidates_ids,
72            exec_outcome,
73        }
74    }
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
78#[ts(export, export_to = "FrontendApi.ts")]
79pub struct SourceDelta {
80    pub text: String,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ts_rs::TS)]
84pub struct SketchCheckpointId(u64);
85
86impl SketchCheckpointId {
87    pub(crate) fn new(n: u64) -> Self {
88        Self(n)
89    }
90}
91
92#[derive(Debug, Clone, Serialize, ts_rs::TS)]
93#[ts(export, export_to = "FrontendApi.ts")]
94#[serde(rename_all = "camelCase")]
95pub struct SketchMutationOutcome {
96    pub source_delta: SourceDelta,
97    pub scene_graph_delta: SceneGraphDelta,
98    pub checkpoint_id: Option<SketchCheckpointId>,
99}
100
101#[derive(Debug, Clone, Serialize, ts_rs::TS)]
102#[ts(export, export_to = "FrontendApi.ts")]
103#[serde(rename_all = "camelCase")]
104pub struct NewSketchOutcome {
105    pub source_delta: SourceDelta,
106    pub scene_graph_delta: SceneGraphDelta,
107    pub sketch_id: ObjectId,
108    pub checkpoint_id: Option<SketchCheckpointId>,
109}
110
111#[derive(Debug, Clone, Serialize, ts_rs::TS)]
112#[ts(export, export_to = "FrontendApi.ts")]
113#[serde(rename_all = "camelCase")]
114pub struct EditSketchOutcome {
115    pub scene_graph_delta: SceneGraphDelta,
116    pub checkpoint_id: Option<SketchCheckpointId>,
117}
118
119#[derive(Debug, Clone, Serialize, ts_rs::TS)]
120#[ts(export, export_to = "FrontendApi.ts")]
121#[serde(rename_all = "camelCase")]
122pub struct RestoreSketchCheckpointOutcome {
123    pub source_delta: SourceDelta,
124    pub scene_graph_delta: SceneGraphDelta,
125}
126
127#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ts_rs::TS)]
128#[ts(export, export_to = "FrontendApi.ts", rename = "ApiVersion")]
129pub struct Version(pub usize);
130
131#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
132#[ts(export, export_to = "FrontendApi.ts", rename = "ApiProjectId")]
133pub struct ProjectId(pub usize);
134
135#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
136#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFileId")]
137pub struct FileId(pub usize);
138
139#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
140#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFile")]
141pub struct File {
142    pub id: FileId,
143    pub path: String,
144    pub text: String,
145}
146
147/// Runtime helpers for [`ObjectKind`].
148pub trait ObjectKindExt {
149    /// What kind of object is this (point, line, arc, etc)
150    /// Suitable for use in user-facing messages.
151    fn human_friendly_kind_with_article(&self) -> &'static str;
152}
153
154impl ObjectKindExt for ObjectKind {
155    fn human_friendly_kind_with_article(&self) -> &'static str {
156        match self {
157            Self::Nil => "a Nil",
158            Self::Plane(..) => "a Plane",
159            Self::Face(..) => "a Face",
160            Self::Wall(..) => "a Wall",
161            Self::Cap(..) => "a Cap",
162            Self::Sketch(..) => "a Sketch",
163            Self::Segment { .. } => "a Segment",
164            Self::Constraint { .. } => "a Constraint",
165        }
166    }
167}
168
169impl TryFrom<crate::std::args::TyF64> for Number {
170    type Error = kcl_api::NumericSuffixTypeConvertError;
171
172    fn try_from(value: crate::std::args::TyF64) -> std::result::Result<Self, Self::Error> {
173        Ok(Number {
174            value: value.n,
175            units: value.ty.try_into()?,
176        })
177    }
178}
179
180/// Runtime helpers for [`Number`].
181pub trait NumberExt {
182    fn round(&self, digits: u8) -> Self;
183}
184
185impl NumberExt for Number {
186    fn round(&self, digits: u8) -> Self {
187        let factor = 10f64.powi(digits as i32);
188        let rounded_value = (self.value * factor).round() / factor;
189        // Don't return negative zero.
190        let value = if rounded_value == -0.0 { 0.0 } else { rounded_value };
191        Number {
192            value,
193            units: self.units,
194        }
195    }
196}
197
198#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
199#[ts(export, export_to = "FrontendApi.ts")]
200pub struct Error {
201    pub msg: String,
202}
203
204impl Error {
205    pub fn file_id_in_use(id: FileId, path: &str) -> Self {
206        Error {
207            msg: format!("File ID already in use: {id:?}, currently used for `{path}`"),
208        }
209    }
210
211    pub fn file_id_not_found(project_id: ProjectId, file_id: FileId) -> Self {
212        Error {
213            msg: format!("File ID not found in project: {file_id:?}, project: {project_id:?}"),
214        }
215    }
216
217    pub fn bad_project(found: ProjectId, expected: Option<ProjectId>) -> Self {
218        let msg = match expected {
219            Some(expected) => format!("Project ID mismatch found: {found:?}, expected: {expected:?}"),
220            None => format!("No open project, found: {found:?}"),
221        };
222        Error { msg }
223    }
224
225    pub fn bad_version(found: Version, expected: Version) -> Self {
226        Error {
227            msg: format!("Version mismatch found: {found:?}, expected: {expected:?}"),
228        }
229    }
230
231    pub fn bad_file(found: FileId, expected: Option<FileId>) -> Self {
232        let msg = match expected {
233            Some(expected) => format!("File ID mismatch found: {found:?}, expected: {expected:?}"),
234            None => format!("File ID not found: {found:?}"),
235        };
236        Error { msg }
237    }
238
239    pub fn serialize(e: impl serde::ser::Error) -> Self {
240        Error {
241            msg: format!(
242                "Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
243            ),
244        }
245    }
246
247    pub fn deserialize(name: &str, e: impl serde::de::Error) -> Self {
248        Error {
249            msg: format!(
250                "Could not deserialize argument `{name}`. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
251            ),
252        }
253    }
254}
255
256pub type Result<T> = std::result::Result<T, Error>;