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#[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 Enum {
39 enum_name: String,
40 variant: String,
41 },
42 SketchVar {
43 value: Box<SketchVarView>,
44 },
45 SketchConstraint {
47 value: JsonValue,
48 },
49 Tuple {
50 value: Vec<KclValueView>,
51 },
52 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 CameraView {
69 value: JsonValue,
70 },
71 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 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#[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#[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 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}