Skip to main content

kcl_api/
kcl_value_view.rs

1use indexmap::IndexMap;
2use kcl_error::ModuleId;
3use kcl_error::SourceRange;
4use schemars::JsonSchema;
5use serde::Deserialize;
6use serde::Serialize;
7use serde_json::Value as JsonValue;
8
9use crate::ArtifactId;
10use crate::NumericType;
11use crate::ObjectId;
12use crate::UnitLength;
13
14pub type KclObjectFields = IndexMap<String, KclValueView>;
15
16/// A serializable, presentational view of any KCL value.
17///
18/// This type deliberately contains no executor state. Runtime values are
19/// converted to it by `kcl-lib` before they cross an API boundary.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
21#[ts(export)]
22#[serde(tag = "type")]
23pub enum KclValueView {
24    Uuid {
25        value: uuid::Uuid,
26    },
27    Bool {
28        value: bool,
29    },
30    Number {
31        value: f64,
32        ty: NumericType,
33    },
34    String {
35        value: String,
36    },
37    /// Exposed by nominal identity, not by the variant's representation.
38    Enum {
39        enum_name: String,
40        variant: String,
41    },
42    SketchVar {
43        value: Box<SketchVarView>,
44    },
45    /// Sketch constraints are currently only shown in the debug memory pane.
46    SketchConstraint {
47        value: JsonValue,
48    },
49    Tuple {
50        value: Vec<KclValueView>,
51    },
52    /// An array where all values have a shared type.
53    HomArray {
54        value: Vec<KclValueView>,
55    },
56    Object {
57        value: KclObjectFields,
58        constrainable: bool,
59    },
60    TagIdentifier(TagIdentifierView),
61    TagDeclarator {
62        value: String,
63    },
64    GdtAnnotation {
65        value: Box<GdtAnnotationView>,
66    },
67    /// Camera values are currently only shown in the debug memory pane.
68    CameraView {
69        value: JsonValue,
70    },
71    /// Named views are consumed through the artifact graph, not program memory.
72    NamedView {
73        value: JsonValue,
74    },
75    Plane {
76        value: Box<PlaneView>,
77    },
78    Face {
79        value: Box<FaceView>,
80    },
81    BoundedEdge {
82        value: BoundedEdgeView,
83    },
84    /// Standalone segments are currently only shown in the debug memory pane.
85    Segment {
86        value: JsonValue,
87    },
88    Sketch {
89        value: Box<SketchView>,
90    },
91    Solid {
92        value: Box<SolidView>,
93    },
94    Helix {
95        value: Box<HelixView>,
96    },
97    ImportedGeometry(ImportedGeometryView),
98    Function {},
99    Module {
100        value: ModuleId,
101    },
102    Type {
103        experimental: bool,
104    },
105    KclNone {},
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
109#[ts(export)]
110#[serde(rename_all = "camelCase")]
111pub struct SketchVarView {
112    pub initial_value: f64,
113    pub ty: NumericType,
114}
115
116#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
117pub enum TagIdentifierViewType {
118    #[default]
119    TagIdentifier,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
123#[ts(export)]
124pub struct TagIdentifierView {
125    #[serde(rename = "type")]
126    #[ts(rename = "type", type = "\"TagIdentifier\"")]
127    pub type_: TagIdentifierViewType,
128    pub value: String,
129}
130
131#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
132pub enum TagDeclaratorViewType {
133    #[default]
134    TagDeclarator,
135}
136
137/// The presentational portion of a tag declaration, including its source location.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
139#[ts(export)]
140#[serde(rename_all = "camelCase")]
141pub struct TagDeclaratorView {
142    pub comment_start: usize,
143    pub end: usize,
144    pub module_id: ModuleId,
145    pub start: usize,
146    #[serde(rename = "type")]
147    #[ts(rename = "type", type = "\"TagDeclarator\"")]
148    pub type_: TagDeclaratorViewType,
149    #[serde(rename = "value")]
150    pub name: String,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    #[ts(optional)]
153    pub digest: Option<[u8; 32]>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
157#[ts(export)]
158#[serde(rename_all = "camelCase")]
159pub struct GdtAnnotationView {
160    pub id: uuid::Uuid,
161}
162
163#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
164#[ts(export)]
165pub struct Point3dView {
166    pub x: f64,
167    pub y: f64,
168    pub z: f64,
169    pub units: Option<UnitLength>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
173#[ts(export)]
174#[serde(rename_all = "camelCase")]
175pub struct PlaneView {
176    pub artifact_id: ArtifactId,
177    pub id: uuid::Uuid,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub object_id: Option<ObjectId>,
180    pub kind: PlaneKindView,
181    pub origin: Point3dView,
182    pub x_axis: Point3dView,
183    pub y_axis: Point3dView,
184    pub z_axis: Point3dView,
185}
186
187#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
188#[ts(export)]
189pub enum PlaneKindView {
190    #[serde(rename = "XY", alias = "xy")]
191    XY,
192    #[serde(rename = "XZ", alias = "xz")]
193    XZ,
194    #[serde(rename = "YZ", alias = "yz")]
195    YZ,
196    Custom,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
200#[ts(export)]
201#[serde(rename_all = "camelCase")]
202pub struct FaceView {
203    pub id: uuid::Uuid,
204    pub artifact_id: ArtifactId,
205    pub object_id: ObjectId,
206    pub value: String,
207    pub x_axis: Point3dView,
208    pub y_axis: Point3dView,
209    pub parent_solid: FaceParentSolidView,
210    pub units: UnitLength,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
214#[ts(export)]
215#[serde(rename_all = "camelCase")]
216pub struct FaceParentSolidView {
217    pub solid_id: uuid::Uuid,
218    pub creator_sketch_id: Option<uuid::Uuid>,
219    pub creator_sketch_is_closed: Option<ProfileClosedView>,
220    #[serde(default, skip_serializing_if = "Vec::is_empty")]
221    pub edge_cut_ids: Vec<uuid::Uuid>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
225#[ts(export)]
226#[serde(rename_all = "camelCase")]
227pub struct BoundedEdgeView {
228    pub face_id: uuid::Uuid,
229    pub edge_id: Option<uuid::Uuid>,
230    pub lower_bound: f32,
231    pub upper_bound: f32,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
235#[ts(export)]
236#[serde(rename_all = "camelCase")]
237pub struct ImportedGeometryView {
238    pub id: uuid::Uuid,
239    pub value: Vec<String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
243#[ts(export)]
244#[serde(rename_all = "camelCase")]
245pub struct HelixView {
246    pub value: uuid::Uuid,
247    pub artifact_id: ArtifactId,
248    pub revolutions: f64,
249    pub angle_start: f64,
250    pub ccw: bool,
251    pub cylinder_id: Option<uuid::Uuid>,
252    pub units: UnitLength,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
256#[ts(export)]
257#[serde(tag = "type", rename_all = "camelCase")]
258pub enum SketchSurfaceView {
259    Plane(Box<PlaneView>),
260    Face(Box<FaceView>),
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
264#[ts(export)]
265#[serde(rename_all = "camelCase")]
266pub struct BasePathView {
267    #[ts(type = "[number, number]")]
268    pub from: [f64; 2],
269    #[ts(type = "[number, number]")]
270    pub to: [f64; 2],
271    pub units: UnitLength,
272    pub tag: Option<TagDeclaratorView>,
273    #[serde(rename = "__geoMeta")]
274    pub geo_meta: GeoMetaView,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
278#[ts(export)]
279#[serde(rename_all = "camelCase")]
280pub struct GeoMetaView {
281    pub id: uuid::Uuid,
282    pub source_range: SourceRange,
283}
284
285/// A sketch path containing the geometry and source data used by the editor.
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
287#[ts(export)]
288#[serde(tag = "type")]
289pub enum PathView {
290    ToPoint {
291        #[serde(flatten)]
292        base: BasePathView,
293    },
294    TangentialArcTo {
295        #[serde(flatten)]
296        base: BasePathView,
297        center: [f64; 2],
298        ccw: bool,
299    },
300    TangentialArc {
301        #[serde(flatten)]
302        base: BasePathView,
303        center: [f64; 2],
304        ccw: bool,
305    },
306    Circle {
307        #[serde(flatten)]
308        base: BasePathView,
309        center: [f64; 2],
310        radius: f64,
311        ccw: bool,
312    },
313    CircleThreePoint {
314        #[serde(flatten)]
315        base: BasePathView,
316        p1: [f64; 2],
317        p2: [f64; 2],
318        p3: [f64; 2],
319    },
320    ArcThreePoint {
321        #[serde(flatten)]
322        base: BasePathView,
323        p1: [f64; 2],
324        p2: [f64; 2],
325        p3: [f64; 2],
326    },
327    Horizontal {
328        #[serde(flatten)]
329        base: BasePathView,
330        x: f64,
331    },
332    AngledLineTo {
333        #[serde(flatten)]
334        base: BasePathView,
335        x: Option<f64>,
336        y: Option<f64>,
337    },
338    Base {
339        #[serde(flatten)]
340        base: BasePathView,
341    },
342    Arc {
343        #[serde(flatten)]
344        base: BasePathView,
345        center: [f64; 2],
346        radius: f64,
347        ccw: bool,
348    },
349    Ellipse {
350        #[serde(flatten)]
351        base: BasePathView,
352        center: [f64; 2],
353        major_axis: [f64; 2],
354        minor_radius: f64,
355        ccw: bool,
356    },
357    Conic {
358        #[serde(flatten)]
359        base: BasePathView,
360    },
361    Bezier {
362        #[serde(flatten)]
363        base: BasePathView,
364        control1: [f64; 2],
365        control2: [f64; 2],
366    },
367}
368
369#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
370#[ts(export)]
371#[serde(rename_all = "camelCase")]
372pub enum ProfileClosedView {
373    No,
374    Maybe,
375    Implicitly,
376    Explicitly,
377}
378
379#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
380pub enum SketchViewType {
381    #[default]
382    Sketch,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
386#[ts(export, rename = "SketchView")]
387#[serde(rename_all = "camelCase")]
388pub struct SketchView {
389    #[serde(rename = "type")]
390    #[ts(rename = "type", type = "\"Sketch\"")]
391    pub type_: SketchViewType,
392    pub id: uuid::Uuid,
393    pub paths: Vec<PathView>,
394    #[serde(default, skip_serializing_if = "Vec::is_empty")]
395    pub inner_paths: Vec<PathView>,
396    pub on: SketchSurfaceView,
397    pub start: BasePathView,
398    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
399    pub tags: IndexMap<String, TagIdentifierView>,
400    pub artifact_id: ArtifactId,
401    pub original_id: uuid::Uuid,
402    pub units: UnitLength,
403    pub is_closed: ProfileClosedView,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
407#[ts(export)]
408#[serde(tag = "type", rename_all = "camelCase")]
409pub enum ExtrudeSurfaceView {
410    ExtrudePlane(SurfaceView),
411    ExtrudeArc(SurfaceView),
412    Chamfer(SurfaceView),
413    Fillet(SurfaceView),
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
417#[ts(export)]
418#[serde(rename_all = "camelCase")]
419pub struct SurfaceView {
420    pub face_id: uuid::Uuid,
421    pub tag: Option<TagDeclaratorView>,
422    #[serde(flatten)]
423    pub geo_meta: GeoMetaView,
424}
425
426#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
427#[ts(export)]
428pub struct NumericValueView {
429    pub n: f64,
430    pub ty: NumericType,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
434#[ts(export)]
435#[serde(tag = "type", rename_all = "camelCase")]
436pub enum EdgeCutView {
437    Fillet {
438        id: uuid::Uuid,
439        radius: NumericValueView,
440        #[serde(rename = "edgeId")]
441        #[ts(rename = "edgeId")]
442        edge_id: uuid::Uuid,
443        tag: Option<TagDeclaratorView>,
444    },
445    Chamfer {
446        id: uuid::Uuid,
447        length: NumericValueView,
448        #[serde(rename = "edgeId")]
449        #[ts(rename = "edgeId")]
450        edge_id: uuid::Uuid,
451        tag: Option<TagDeclaratorView>,
452    },
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
456#[ts(export)]
457#[serde(tag = "creatorType", rename_all = "camelCase")]
458pub enum SolidCreatorView {
459    Sketch(SketchView),
460    Face {
461        face_id: uuid::Uuid,
462        solid_id: uuid::Uuid,
463        sketch: SketchView,
464    },
465    Edge {
466        edge_id: uuid::Uuid,
467        body_id: uuid::Uuid,
468    },
469    Procedural,
470}
471
472#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
473pub enum SolidViewType {
474    #[default]
475    Solid,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
479#[ts(export, rename = "SolidView")]
480#[serde(rename_all = "camelCase")]
481pub struct SolidView {
482    #[serde(rename = "type")]
483    #[ts(rename = "type", type = "\"Solid\"")]
484    pub type_: SolidViewType,
485    pub id: uuid::Uuid,
486    pub original_id: uuid::Uuid,
487    pub topology_id: uuid::Uuid,
488    pub artifact_id: ArtifactId,
489    /// Surface summaries are retained for the debug memory pane.
490    pub value: Vec<ExtrudeSurfaceView>,
491    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
492    pub faces: IndexMap<String, TagIdentifierView>,
493    #[serde(rename = "sketch")]
494    pub creator: SolidCreatorView,
495    pub start_cap_id: Option<uuid::Uuid>,
496    pub end_cap_id: Option<uuid::Uuid>,
497    #[serde(default, skip_serializing_if = "Vec::is_empty")]
498    pub edge_cuts: Vec<EdgeCutView>,
499    pub units: UnitLength,
500    pub sectional: bool,
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn value_view_round_trips_through_json() {
509        let value = KclValueView::Object {
510            value: IndexMap::from([
511                (
512                    "length".to_owned(),
513                    KclValueView::Number {
514                        value: 12.5,
515                        ty: NumericType::default(),
516                    },
517                ),
518                (
519                    "tag".to_owned(),
520                    KclValueView::TagDeclarator {
521                        value: "edge01".to_owned(),
522                    },
523                ),
524            ]),
525            constrainable: false,
526        };
527
528        let json = serde_json::to_value(&value).unwrap();
529        let round_trip = serde_json::from_value(json).unwrap();
530        assert_eq!(value, round_trip);
531    }
532
533    #[test]
534    fn tag_declarator_keeps_the_existing_wire_shape() {
535        let value = KclValueView::TagDeclarator {
536            value: "edge01".to_owned(),
537        };
538
539        assert_eq!(
540            serde_json::to_value(value).unwrap(),
541            serde_json::json!({ "type": "TagDeclarator", "value": "edge01" })
542        );
543    }
544
545    #[test]
546    fn nested_tag_views_keep_their_discriminators_and_source_location() {
547        let identifier = TagIdentifierView {
548            type_: TagIdentifierViewType::TagIdentifier,
549            value: "edge01".to_owned(),
550        };
551        assert_eq!(
552            serde_json::to_value(identifier).unwrap(),
553            serde_json::json!({ "type": "TagIdentifier", "value": "edge01" })
554        );
555
556        let declarator = TagDeclaratorView {
557            comment_start: 4,
558            end: 10,
559            module_id: ModuleId::default(),
560            start: 5,
561            type_: TagDeclaratorViewType::TagDeclarator,
562            name: "edge01".to_owned(),
563            digest: None,
564        };
565        assert_eq!(
566            serde_json::to_value(declarator).unwrap(),
567            serde_json::json!({
568                "commentStart": 4,
569                "end": 10,
570                "moduleId": 0,
571                "start": 5,
572                "type": "TagDeclarator",
573                "value": "edge01"
574            })
575        );
576    }
577
578    #[test]
579    fn edge_cut_views_keep_dimensions_and_camel_case_edge_ids() {
580        let edge_id = uuid::Uuid::nil();
581        let value = EdgeCutView::Fillet {
582            id: uuid::Uuid::nil(),
583            radius: NumericValueView {
584                n: 2.0,
585                ty: NumericType::default(),
586            },
587            edge_id,
588            tag: None,
589        };
590
591        let json = serde_json::to_value(value).unwrap();
592        assert_eq!(json["radius"]["n"], 2.0);
593        assert_eq!(json["edgeId"], edge_id.to_string());
594        assert!(json.get("edge_id").is_none());
595
596        let value = EdgeCutView::Chamfer {
597            id: uuid::Uuid::nil(),
598            length: NumericValueView {
599                n: 3.0,
600                ty: NumericType::default(),
601            },
602            edge_id,
603            tag: None,
604        };
605        let json = serde_json::to_value(value).unwrap();
606        assert_eq!(json["length"]["n"], 3.0);
607        assert_eq!(json["edgeId"], edge_id.to_string());
608    }
609
610    #[test]
611    fn face_parent_solid_view_keeps_face_provenance() {
612        let solid_id = uuid::Uuid::nil();
613        let value = FaceParentSolidView {
614            solid_id,
615            creator_sketch_id: Some(solid_id),
616            creator_sketch_is_closed: Some(ProfileClosedView::Explicitly),
617            edge_cut_ids: vec![solid_id],
618        };
619
620        let json = serde_json::to_value(value).unwrap();
621        assert_eq!(json["solidId"], solid_id.to_string());
622        assert_eq!(json["creatorSketchId"], solid_id.to_string());
623        assert_eq!(json["creatorSketchIsClosed"], "explicitly");
624        assert_eq!(json["edgeCutIds"][0], solid_id.to_string());
625    }
626}