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