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