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