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 {
61 value: String,
62 },
63 TagDeclarator {
64 value: String,
65 },
66 GdtAnnotation {
67 value: Box<GdtAnnotationView>,
68 },
69 CameraView {
71 value: JsonValue,
72 },
73 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 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#[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#[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 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}