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    PrimitiveFace(PrimitiveFacePlane),
220}
221
222/// An indexed face on a solid which has not yet been materialized as a KCL
223/// `Face` value.
224#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
225#[ts(export, export_to = "FrontendApi.ts")]
226#[serde(rename_all = "camelCase")]
227pub struct PrimitiveFacePlane {
228    /// Engine ID of the solid which owns the face.
229    pub solid_id: uuid::Uuid,
230    pub index: usize,
231}
232
233#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
234#[ts(export, export_to = "FrontendApi.ts", rename = "ApiFace")]
235#[serde(rename_all = "camelCase")]
236pub struct Face {
237    pub id: ObjectId,
238}
239
240#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
241#[ts(export, export_to = "FrontendApi.ts", rename = "ApiWall")]
242#[serde(rename_all = "camelCase")]
243pub struct Wall {
244    pub id: ObjectId,
245    #[serde(skip_deserializing)]
246    pub source: WallSource,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    #[ts(optional)]
249    pub solid_output_index: Option<usize>,
250}
251
252#[derive(Debug, Clone, Default, PartialEq, Serialize, ts_rs::TS)]
253#[ts(export, export_to = "FrontendApi.ts", rename = "ApiWallSource")]
254#[serde(rename_all = "camelCase")]
255pub struct WallSource {
256    pub solid: SourceRefRange,
257    pub sweep: SourceRefRange,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    #[ts(optional)]
260    pub path: Option<SourceRefRange>,
261    pub segment: SourceRefRange,
262}
263
264#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
265#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCap")]
266#[serde(rename_all = "camelCase")]
267pub struct Cap {
268    pub id: ObjectId,
269    pub kind: CapKind,
270    #[serde(skip_deserializing)]
271    pub source: CapSource,
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    #[ts(optional)]
274    pub solid_output_index: Option<usize>,
275}
276
277#[derive(Debug, Clone, Default, PartialEq, Serialize, ts_rs::TS)]
278#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCapSource")]
279#[serde(rename_all = "camelCase")]
280pub struct CapSource {
281    pub solid: SourceRefRange,
282    pub sweep: SourceRefRange,
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ts_rs::TS)]
286#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCapKind")]
287#[serde(rename_all = "camelCase")]
288pub enum CapKind {
289    Start,
290    End,
291}
292
293#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
294#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSourceRef")]
295#[serde(tag = "type")]
296pub enum SourceRef {
297    Simple {
298        range: SourceRange,
299        node_path: Option<NodePath>,
300    },
301    BackTrace {
302        ranges: Vec<(SourceRange, Option<NodePath>)>,
303    },
304}
305
306#[derive(Debug, Clone, Default, PartialEq, Serialize, ts_rs::TS)]
307#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSourceRefRange")]
308#[serde(rename_all = "camelCase")]
309pub struct SourceRefRange {
310    pub range: SourceRange,
311    pub node_path: Option<NodePath>,
312}
313
314impl From<SourceRange> for SourceRef {
315    fn from(value: SourceRange) -> Self {
316        Self::Simple {
317            range: value,
318            node_path: None,
319        }
320    }
321}
322
323impl SourceRef {
324    pub fn new(range: SourceRange, node_path: Option<NodePath>) -> Self {
325        Self::Simple { range, node_path }
326    }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
330#[ts(export, export_to = "FrontendApi.ts")]
331pub struct Number {
332    pub value: f64,
333    pub units: NumericSuffix,
334}
335
336impl TryFrom<crate::std::args::TyF64> for Number {
337    type Error = crate::execution::types::NumericSuffixTypeConvertError;
338
339    fn try_from(value: crate::std::args::TyF64) -> std::result::Result<Self, Self::Error> {
340        Ok(Number {
341            value: value.n,
342            units: value.ty.try_into()?,
343        })
344    }
345}
346
347impl Number {
348    pub fn round(&self, digits: u8) -> Self {
349        let factor = 10f64.powi(digits as i32);
350        let rounded_value = (self.value * factor).round() / factor;
351        // Don't return negative zero.
352        let value = if rounded_value == -0.0 { 0.0 } else { rounded_value };
353        Number {
354            value,
355            units: self.units,
356        }
357    }
358}
359
360impl From<(f64, UnitLength)> for Number {
361    fn from((value, units): (f64, UnitLength)) -> Self {
362        // Direct conversion from UnitLength to NumericSuffix (never panics)
363        // The From<UnitLength> for NumericSuffix impl is in execution::types
364        let units_suffix = NumericSuffix::from(units);
365        Number {
366            value,
367            units: units_suffix,
368        }
369    }
370}
371
372#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
373#[ts(export, export_to = "FrontendApi.ts")]
374#[serde(tag = "type")]
375pub enum Expr {
376    Number(Number),
377    Var(Number),
378    Variable(String),
379}
380
381#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
382#[ts(export, export_to = "FrontendApi.ts")]
383pub struct Error {
384    pub msg: String,
385}
386
387impl Error {
388    pub fn file_id_in_use(id: FileId, path: &str) -> Self {
389        Error {
390            msg: format!("File ID already in use: {id:?}, currently used for `{path}`"),
391        }
392    }
393
394    pub fn file_id_not_found(project_id: ProjectId, file_id: FileId) -> Self {
395        Error {
396            msg: format!("File ID not found in project: {file_id:?}, project: {project_id:?}"),
397        }
398    }
399
400    pub fn bad_project(found: ProjectId, expected: Option<ProjectId>) -> Self {
401        let msg = match expected {
402            Some(expected) => format!("Project ID mismatch found: {found:?}, expected: {expected:?}"),
403            None => format!("No open project, found: {found:?}"),
404        };
405        Error { msg }
406    }
407
408    pub fn bad_version(found: Version, expected: Version) -> Self {
409        Error {
410            msg: format!("Version mismatch found: {found:?}, expected: {expected:?}"),
411        }
412    }
413
414    pub fn bad_file(found: FileId, expected: Option<FileId>) -> Self {
415        let msg = match expected {
416            Some(expected) => format!("File ID mismatch found: {found:?}, expected: {expected:?}"),
417            None => format!("File ID not found: {found:?}"),
418        };
419        Error { msg }
420    }
421
422    pub fn serialize(e: impl serde::ser::Error) -> Self {
423        Error {
424            msg: format!(
425                "Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
426            ),
427        }
428    }
429
430    pub fn deserialize(name: &str, e: impl serde::de::Error) -> Self {
431        Error {
432            msg: format!(
433                "Could not deserialize argument `{name}`. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"
434            ),
435        }
436    }
437}
438
439pub type Result<T> = std::result::Result<T, Error>;