kcl_lib/execution/
geometry.rs

1use std::ops::{Add, AddAssign, Mul};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds as kcmc;
6use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq, ModelingCmd};
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    engine::{PlaneName, DEFAULT_PLANE_INFO},
13    errors::{KclError, KclErrorDetails},
14    execution::{
15        types::NumericType, ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen,
16    },
17    parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
18    std::{args::TyF64, sketch::PlaneData},
19};
20
21type Point3D = kcmc::shared::Point3d<f64>;
22
23/// A geometry.
24#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
25#[ts(export)]
26#[serde(tag = "type")]
27#[allow(clippy::large_enum_variant)]
28pub enum Geometry {
29    Sketch(Sketch),
30    Solid(Solid),
31}
32
33impl Geometry {
34    pub fn id(&self) -> uuid::Uuid {
35        match self {
36            Geometry::Sketch(s) => s.id,
37            Geometry::Solid(e) => e.id,
38        }
39    }
40
41    /// If this geometry is the result of a pattern, then return the ID of
42    /// the original sketch which was patterned.
43    /// Equivalent to the `id()` method if this isn't a pattern.
44    pub fn original_id(&self) -> uuid::Uuid {
45        match self {
46            Geometry::Sketch(s) => s.original_id,
47            Geometry::Solid(e) => e.sketch.original_id,
48        }
49    }
50}
51
52/// A geometry including an imported geometry.
53#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
54#[ts(export)]
55#[serde(tag = "type")]
56#[allow(clippy::large_enum_variant)]
57pub enum GeometryWithImportedGeometry {
58    Sketch(Sketch),
59    Solid(Solid),
60    ImportedGeometry(Box<ImportedGeometry>),
61}
62
63impl GeometryWithImportedGeometry {
64    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
65        match self {
66            GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
67            GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
68            GeometryWithImportedGeometry::ImportedGeometry(i) => {
69                let id = i.id(ctx).await?;
70                Ok(id)
71            }
72        }
73    }
74}
75
76/// A set of geometry.
77#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
78#[ts(export)]
79#[serde(tag = "type")]
80#[allow(clippy::vec_box)]
81pub enum Geometries {
82    Sketches(Vec<Sketch>),
83    Solids(Vec<Solid>),
84}
85
86impl From<Geometry> for Geometries {
87    fn from(value: Geometry) -> Self {
88        match value {
89            Geometry::Sketch(x) => Self::Sketches(vec![x]),
90            Geometry::Solid(x) => Self::Solids(vec![x]),
91        }
92    }
93}
94
95/// Data for an imported geometry.
96#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
97#[ts(export)]
98#[serde(rename_all = "camelCase")]
99pub struct ImportedGeometry {
100    /// The ID of the imported geometry.
101    pub id: uuid::Uuid,
102    /// The original file paths.
103    pub value: Vec<String>,
104    #[serde(skip)]
105    pub meta: Vec<Metadata>,
106    /// If the imported geometry has completed.
107    #[serde(skip)]
108    completed: bool,
109}
110
111impl ImportedGeometry {
112    pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
113        Self {
114            id,
115            value,
116            meta,
117            completed: false,
118        }
119    }
120
121    async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
122        if self.completed {
123            return Ok(());
124        }
125
126        ctx.engine
127            .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
128            .await?;
129
130        self.completed = true;
131
132        Ok(())
133    }
134
135    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
136        if !self.completed {
137            self.wait_for_finish(ctx).await?;
138        }
139
140        Ok(self.id)
141    }
142}
143
144/// Data for a solid, sketch, or an imported geometry.
145#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
146#[ts(export)]
147#[serde(tag = "type", rename_all = "camelCase")]
148#[allow(clippy::vec_box)]
149pub enum SolidOrSketchOrImportedGeometry {
150    ImportedGeometry(Box<ImportedGeometry>),
151    SolidSet(Vec<Solid>),
152    SketchSet(Vec<Sketch>),
153}
154
155impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
156    fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
157        match value {
158            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
159            SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
160                if s.len() == 1 {
161                    crate::execution::KclValue::Solid {
162                        value: Box::new(s.pop().unwrap()),
163                    }
164                } else {
165                    crate::execution::KclValue::HomArray {
166                        value: s
167                            .into_iter()
168                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
169                            .collect(),
170                        ty: crate::execution::types::RuntimeType::solid(),
171                    }
172                }
173            }
174            SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
175                if s.len() == 1 {
176                    crate::execution::KclValue::Sketch {
177                        value: Box::new(s.pop().unwrap()),
178                    }
179                } else {
180                    crate::execution::KclValue::HomArray {
181                        value: s
182                            .into_iter()
183                            .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
184                            .collect(),
185                        ty: crate::execution::types::RuntimeType::sketch(),
186                    }
187                }
188            }
189        }
190    }
191}
192
193impl SolidOrSketchOrImportedGeometry {
194    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
195        match self {
196            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
197                let id = s.id(ctx).await?;
198
199                Ok(vec![id])
200            }
201            SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
202            SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
203        }
204    }
205}
206
207/// Data for a solid or an imported geometry.
208#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
209#[ts(export)]
210#[serde(tag = "type", rename_all = "camelCase")]
211#[allow(clippy::vec_box)]
212pub enum SolidOrImportedGeometry {
213    ImportedGeometry(Box<ImportedGeometry>),
214    SolidSet(Vec<Solid>),
215}
216
217impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
218    fn from(value: SolidOrImportedGeometry) -> Self {
219        match value {
220            SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
221            SolidOrImportedGeometry::SolidSet(mut s) => {
222                if s.len() == 1 {
223                    crate::execution::KclValue::Solid {
224                        value: Box::new(s.pop().unwrap()),
225                    }
226                } else {
227                    crate::execution::KclValue::HomArray {
228                        value: s
229                            .into_iter()
230                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
231                            .collect(),
232                        ty: crate::execution::types::RuntimeType::solid(),
233                    }
234                }
235            }
236        }
237    }
238}
239
240impl SolidOrImportedGeometry {
241    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
242        match self {
243            SolidOrImportedGeometry::ImportedGeometry(s) => {
244                let id = s.id(ctx).await?;
245
246                Ok(vec![id])
247            }
248            SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
249        }
250    }
251}
252
253/// A helix.
254#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
255#[ts(export)]
256#[serde(rename_all = "camelCase")]
257pub struct Helix {
258    /// The id of the helix.
259    pub value: uuid::Uuid,
260    /// The artifact ID.
261    pub artifact_id: ArtifactId,
262    /// Number of revolutions.
263    pub revolutions: f64,
264    /// Start angle (in degrees).
265    pub angle_start: f64,
266    /// Is the helix rotation counter clockwise?
267    pub ccw: bool,
268    /// The cylinder the helix was created on.
269    pub cylinder_id: Option<uuid::Uuid>,
270    pub units: UnitLen,
271    #[serde(skip)]
272    pub meta: Vec<Metadata>,
273}
274
275#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
276#[ts(export)]
277#[serde(rename_all = "camelCase")]
278pub struct Plane {
279    /// The id of the plane.
280    pub id: uuid::Uuid,
281    /// The artifact ID.
282    pub artifact_id: ArtifactId,
283    // The code for the plane either a string or custom.
284    pub value: PlaneType,
285    /// The information for the plane.
286    #[serde(flatten)]
287    pub info: PlaneInfo,
288    #[serde(skip)]
289    pub meta: Vec<Metadata>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
293#[ts(export)]
294#[serde(rename_all = "camelCase")]
295pub struct PlaneInfo {
296    /// Origin of the plane.
297    pub origin: Point3d,
298    /// What should the plane's X axis be?
299    pub x_axis: Point3d,
300    /// What should the plane's Y axis be?
301    pub y_axis: Point3d,
302}
303
304impl PlaneInfo {
305    pub(crate) fn into_plane_data(self) -> PlaneData {
306        if self.origin.is_zero() {
307            match self {
308                Self {
309                    origin:
310                        Point3d {
311                            x: 0.0,
312                            y: 0.0,
313                            z: 0.0,
314                            units: UnitLen::Mm,
315                        },
316                    x_axis:
317                        Point3d {
318                            x: 1.0,
319                            y: 0.0,
320                            z: 0.0,
321                            units: _,
322                        },
323                    y_axis:
324                        Point3d {
325                            x: 0.0,
326                            y: 1.0,
327                            z: 0.0,
328                            units: _,
329                        },
330                } => return PlaneData::XY,
331                Self {
332                    origin:
333                        Point3d {
334                            x: 0.0,
335                            y: 0.0,
336                            z: 0.0,
337                            units: UnitLen::Mm,
338                        },
339                    x_axis:
340                        Point3d {
341                            x: -1.0,
342                            y: 0.0,
343                            z: 0.0,
344                            units: _,
345                        },
346                    y_axis:
347                        Point3d {
348                            x: 0.0,
349                            y: 1.0,
350                            z: 0.0,
351                            units: _,
352                        },
353                } => return PlaneData::NegXY,
354                Self {
355                    origin:
356                        Point3d {
357                            x: 0.0,
358                            y: 0.0,
359                            z: 0.0,
360                            units: UnitLen::Mm,
361                        },
362                    x_axis:
363                        Point3d {
364                            x: 1.0,
365                            y: 0.0,
366                            z: 0.0,
367                            units: _,
368                        },
369                    y_axis:
370                        Point3d {
371                            x: 0.0,
372                            y: 0.0,
373                            z: 1.0,
374                            units: _,
375                        },
376                } => return PlaneData::XZ,
377                Self {
378                    origin:
379                        Point3d {
380                            x: 0.0,
381                            y: 0.0,
382                            z: 0.0,
383                            units: UnitLen::Mm,
384                        },
385                    x_axis:
386                        Point3d {
387                            x: -1.0,
388                            y: 0.0,
389                            z: 0.0,
390                            units: _,
391                        },
392                    y_axis:
393                        Point3d {
394                            x: 0.0,
395                            y: 0.0,
396                            z: 1.0,
397                            units: _,
398                        },
399                } => return PlaneData::NegXZ,
400                Self {
401                    origin:
402                        Point3d {
403                            x: 0.0,
404                            y: 0.0,
405                            z: 0.0,
406                            units: UnitLen::Mm,
407                        },
408                    x_axis:
409                        Point3d {
410                            x: 0.0,
411                            y: 1.0,
412                            z: 0.0,
413                            units: _,
414                        },
415                    y_axis:
416                        Point3d {
417                            x: 0.0,
418                            y: 0.0,
419                            z: 1.0,
420                            units: _,
421                        },
422                } => return PlaneData::YZ,
423                Self {
424                    origin:
425                        Point3d {
426                            x: 0.0,
427                            y: 0.0,
428                            z: 0.0,
429                            units: UnitLen::Mm,
430                        },
431                    x_axis:
432                        Point3d {
433                            x: 0.0,
434                            y: -1.0,
435                            z: 0.0,
436                            units: _,
437                        },
438                    y_axis:
439                        Point3d {
440                            x: 0.0,
441                            y: 0.0,
442                            z: 1.0,
443                            units: _,
444                        },
445                } => return PlaneData::NegYZ,
446                _ => {}
447            }
448        }
449
450        PlaneData::Plane(Self {
451            origin: self.origin,
452            x_axis: self.x_axis,
453            y_axis: self.y_axis,
454        })
455    }
456}
457
458impl TryFrom<PlaneData> for PlaneInfo {
459    type Error = KclError;
460
461    fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
462        if let PlaneData::Plane(info) = value {
463            return Ok(info);
464        }
465        let name = match value {
466            PlaneData::XY => PlaneName::Xy,
467            PlaneData::NegXY => PlaneName::NegXy,
468            PlaneData::XZ => PlaneName::Xz,
469            PlaneData::NegXZ => PlaneName::NegXz,
470            PlaneData::YZ => PlaneName::Yz,
471            PlaneData::NegYZ => PlaneName::NegYz,
472            PlaneData::Plane(_) => {
473                // We will never get here since we already checked for PlaneData::Plane.
474                return Err(KclError::new_internal(KclErrorDetails::new(
475                    format!("PlaneData {:?} not found", value),
476                    Default::default(),
477                )));
478            }
479        };
480
481        let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
482            KclError::new_internal(KclErrorDetails::new(
483                format!("Plane {} not found", name),
484                Default::default(),
485            ))
486        })?;
487
488        Ok(info.clone())
489    }
490}
491
492impl From<PlaneData> for PlaneType {
493    fn from(value: PlaneData) -> Self {
494        match value {
495            PlaneData::XY => PlaneType::XY,
496            PlaneData::NegXY => PlaneType::XY,
497            PlaneData::XZ => PlaneType::XZ,
498            PlaneData::NegXZ => PlaneType::XZ,
499            PlaneData::YZ => PlaneType::YZ,
500            PlaneData::NegYZ => PlaneType::YZ,
501            PlaneData::Plane(_) => PlaneType::Custom,
502        }
503    }
504}
505
506impl Plane {
507    pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
508        let id = exec_state.next_uuid();
509        Ok(Plane {
510            id,
511            artifact_id: id.into(),
512            info: PlaneInfo::try_from(value.clone())?,
513            value: value.into(),
514            meta: vec![],
515        })
516    }
517
518    /// The standard planes are XY, YZ and XZ (in both positive and negative)
519    pub fn is_standard(&self) -> bool {
520        !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
521    }
522}
523
524/// A face.
525#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
526#[ts(export)]
527#[serde(rename_all = "camelCase")]
528pub struct Face {
529    /// The id of the face.
530    pub id: uuid::Uuid,
531    /// The artifact ID.
532    pub artifact_id: ArtifactId,
533    /// The tag of the face.
534    pub value: String,
535    /// What should the face's X axis be?
536    pub x_axis: Point3d,
537    /// What should the face's Y axis be?
538    pub y_axis: Point3d,
539    /// The solid the face is on.
540    pub solid: Box<Solid>,
541    pub units: UnitLen,
542    #[serde(skip)]
543    pub meta: Vec<Metadata>,
544}
545
546/// Type for a plane.
547#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
548#[ts(export)]
549#[display(style = "camelCase")]
550pub enum PlaneType {
551    #[serde(rename = "XY", alias = "xy")]
552    #[display("XY")]
553    XY,
554    #[serde(rename = "XZ", alias = "xz")]
555    #[display("XZ")]
556    XZ,
557    #[serde(rename = "YZ", alias = "yz")]
558    #[display("YZ")]
559    YZ,
560    /// A custom plane.
561    #[display("Custom")]
562    Custom,
563    /// A custom plane which has not been sent to the engine. It must be sent before it is used.
564    #[display("Uninit")]
565    Uninit,
566}
567
568#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
569#[ts(export)]
570#[serde(tag = "type", rename_all = "camelCase")]
571pub struct Sketch {
572    /// The id of the sketch (this will change when the engine's reference to it changes).
573    pub id: uuid::Uuid,
574    /// The paths in the sketch.
575    pub paths: Vec<Path>,
576    /// What the sketch is on (can be a plane or a face).
577    pub on: SketchSurface,
578    /// The starting path.
579    pub start: BasePath,
580    /// Tag identifiers that have been declared in this sketch.
581    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
582    pub tags: IndexMap<String, TagIdentifier>,
583    /// The original id of the sketch. This stays the same even if the sketch is
584    /// is sketched on face etc.
585    pub artifact_id: ArtifactId,
586    #[ts(skip)]
587    pub original_id: uuid::Uuid,
588    /// If the sketch includes a mirror.
589    #[serde(skip)]
590    pub mirror: Option<uuid::Uuid>,
591    pub units: UnitLen,
592    /// Metadata.
593    #[serde(skip)]
594    pub meta: Vec<Metadata>,
595}
596
597impl Sketch {
598    // Tell the engine to enter sketch mode on the sketch.
599    // Run a specific command, then exit sketch mode.
600    pub(crate) fn build_sketch_mode_cmds(
601        &self,
602        exec_state: &mut ExecState,
603        inner_cmd: ModelingCmdReq,
604    ) -> Vec<ModelingCmdReq> {
605        vec![
606            // Before we extrude, we need to enable the sketch mode.
607            // We do this here in case extrude is called out of order.
608            ModelingCmdReq {
609                cmd: ModelingCmd::from(mcmd::EnableSketchMode {
610                    animated: false,
611                    ortho: false,
612                    entity_id: self.on.id(),
613                    adjust_camera: false,
614                    planar_normal: if let SketchSurface::Plane(plane) = &self.on {
615                        // We pass in the normal for the plane here.
616                        let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
617                        Some(normal.into())
618                    } else {
619                        None
620                    },
621                }),
622                cmd_id: exec_state.next_uuid().into(),
623            },
624            inner_cmd,
625            ModelingCmdReq {
626                cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
627                cmd_id: exec_state.next_uuid().into(),
628            },
629        ]
630    }
631}
632
633/// A sketch type.
634#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
635#[ts(export)]
636#[serde(tag = "type", rename_all = "camelCase")]
637pub enum SketchSurface {
638    Plane(Box<Plane>),
639    Face(Box<Face>),
640}
641
642impl SketchSurface {
643    pub(crate) fn id(&self) -> uuid::Uuid {
644        match self {
645            SketchSurface::Plane(plane) => plane.id,
646            SketchSurface::Face(face) => face.id,
647        }
648    }
649    pub(crate) fn x_axis(&self) -> Point3d {
650        match self {
651            SketchSurface::Plane(plane) => plane.info.x_axis,
652            SketchSurface::Face(face) => face.x_axis,
653        }
654    }
655    pub(crate) fn y_axis(&self) -> Point3d {
656        match self {
657            SketchSurface::Plane(plane) => plane.info.y_axis,
658            SketchSurface::Face(face) => face.y_axis,
659        }
660    }
661}
662
663#[derive(Debug, Clone)]
664pub(crate) enum GetTangentialInfoFromPathsResult {
665    PreviousPoint([f64; 2]),
666    Arc { center: [f64; 2], ccw: bool },
667    Circle { center: [f64; 2], ccw: bool, radius: f64 },
668}
669
670impl GetTangentialInfoFromPathsResult {
671    pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
672        match self {
673            GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
674            GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
675                crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
676            }
677            // The circle always starts at 0 degrees, so a suitable tangent
678            // point is either directly above or below.
679            GetTangentialInfoFromPathsResult::Circle {
680                center, radius, ccw, ..
681            } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
682        }
683    }
684}
685
686impl Sketch {
687    pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
688        let mut tag_identifier: TagIdentifier = tag.into();
689        let base = current_path.get_base();
690        tag_identifier.info.push((
691            exec_state.stack().current_epoch(),
692            TagEngineInfo {
693                id: base.geo_meta.id,
694                sketch: self.id,
695                path: Some(current_path.clone()),
696                surface: None,
697            },
698        ));
699
700        self.tags.insert(tag.name.to_string(), tag_identifier);
701    }
702
703    pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
704        for t in tags {
705            match self.tags.get_mut(&t.value) {
706                Some(id) => {
707                    id.merge_info(t);
708                }
709                None => {
710                    self.tags.insert(t.value.clone(), t.clone());
711                }
712            }
713        }
714    }
715
716    /// Get the path most recently sketched.
717    pub(crate) fn latest_path(&self) -> Option<&Path> {
718        self.paths.last()
719    }
720
721    /// The "pen" is an imaginary pen drawing the path.
722    /// This gets the current point the pen is hovering over, i.e. the point
723    /// where the last path segment ends, and the next path segment will begin.
724    pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
725        let Some(path) = self.latest_path() else {
726            return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
727        };
728
729        let to = path.get_base().to;
730        Ok(Point2d::new(to[0], to[1], path.get_base().units))
731    }
732
733    pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
734        let Some(path) = self.latest_path() else {
735            return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
736        };
737        path.get_tangential_info()
738    }
739}
740
741#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
742#[ts(export)]
743#[serde(tag = "type", rename_all = "camelCase")]
744pub struct Solid {
745    /// The id of the solid.
746    pub id: uuid::Uuid,
747    /// The artifact ID of the solid.  Unlike `id`, this doesn't change.
748    pub artifact_id: ArtifactId,
749    /// The extrude surfaces.
750    pub value: Vec<ExtrudeSurface>,
751    /// The sketch.
752    pub sketch: Sketch,
753    /// The height of the solid.
754    pub height: f64,
755    /// The id of the extrusion start cap
756    pub start_cap_id: Option<uuid::Uuid>,
757    /// The id of the extrusion end cap
758    pub end_cap_id: Option<uuid::Uuid>,
759    /// Chamfers or fillets on this solid.
760    #[serde(default, skip_serializing_if = "Vec::is_empty")]
761    pub edge_cuts: Vec<EdgeCut>,
762    /// The units of the solid.
763    pub units: UnitLen,
764    /// Is this a sectional solid?
765    pub sectional: bool,
766    /// Metadata.
767    #[serde(skip)]
768    pub meta: Vec<Metadata>,
769}
770
771impl Solid {
772    pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
773        self.edge_cuts.iter().map(|foc| foc.id())
774    }
775
776    pub(crate) fn height_in_mm(&self) -> f64 {
777        self.units.adjust_to(self.height, UnitLen::Mm).0
778    }
779}
780
781/// A fillet or a chamfer.
782#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
783#[ts(export)]
784#[serde(tag = "type", rename_all = "camelCase")]
785pub enum EdgeCut {
786    /// A fillet.
787    Fillet {
788        /// The id of the engine command that called this fillet.
789        id: uuid::Uuid,
790        radius: TyF64,
791        /// The engine id of the edge to fillet.
792        #[serde(rename = "edgeId")]
793        edge_id: uuid::Uuid,
794        tag: Box<Option<TagNode>>,
795    },
796    /// A chamfer.
797    Chamfer {
798        /// The id of the engine command that called this chamfer.
799        id: uuid::Uuid,
800        length: TyF64,
801        /// The engine id of the edge to chamfer.
802        #[serde(rename = "edgeId")]
803        edge_id: uuid::Uuid,
804        tag: Box<Option<TagNode>>,
805    },
806}
807
808impl EdgeCut {
809    pub fn id(&self) -> uuid::Uuid {
810        match self {
811            EdgeCut::Fillet { id, .. } => *id,
812            EdgeCut::Chamfer { id, .. } => *id,
813        }
814    }
815
816    pub fn set_id(&mut self, id: uuid::Uuid) {
817        match self {
818            EdgeCut::Fillet { id: ref mut i, .. } => *i = id,
819            EdgeCut::Chamfer { id: ref mut i, .. } => *i = id,
820        }
821    }
822
823    pub fn edge_id(&self) -> uuid::Uuid {
824        match self {
825            EdgeCut::Fillet { edge_id, .. } => *edge_id,
826            EdgeCut::Chamfer { edge_id, .. } => *edge_id,
827        }
828    }
829
830    pub fn set_edge_id(&mut self, id: uuid::Uuid) {
831        match self {
832            EdgeCut::Fillet { edge_id: ref mut i, .. } => *i = id,
833            EdgeCut::Chamfer { edge_id: ref mut i, .. } => *i = id,
834        }
835    }
836
837    pub fn tag(&self) -> Option<TagNode> {
838        match self {
839            EdgeCut::Fillet { tag, .. } => *tag.clone(),
840            EdgeCut::Chamfer { tag, .. } => *tag.clone(),
841        }
842    }
843}
844
845#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
846#[ts(export)]
847pub struct Point2d {
848    pub x: f64,
849    pub y: f64,
850    pub units: UnitLen,
851}
852
853impl Point2d {
854    pub const ZERO: Self = Self {
855        x: 0.0,
856        y: 0.0,
857        units: UnitLen::Mm,
858    };
859
860    pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
861        Self { x, y, units }
862    }
863
864    pub fn into_x(self) -> TyF64 {
865        TyF64::new(self.x, self.units.into())
866    }
867
868    pub fn into_y(self) -> TyF64 {
869        TyF64::new(self.y, self.units.into())
870    }
871
872    pub fn ignore_units(self) -> [f64; 2] {
873        [self.x, self.y]
874    }
875}
876
877#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
878#[ts(export)]
879pub struct Point3d {
880    pub x: f64,
881    pub y: f64,
882    pub z: f64,
883    pub units: UnitLen,
884}
885
886impl Point3d {
887    pub const ZERO: Self = Self {
888        x: 0.0,
889        y: 0.0,
890        z: 0.0,
891        units: UnitLen::Mm,
892    };
893
894    pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
895        Self { x, y, z, units }
896    }
897
898    pub const fn is_zero(&self) -> bool {
899        self.x == 0.0 && self.y == 0.0 && self.z == 0.0
900    }
901
902    /// Calculate the cross product of this vector with another.
903    ///
904    /// This should only be applied to axes or other vectors which represent only a direction (and
905    /// no magnitude) since units are ignored.
906    pub fn axes_cross_product(&self, other: &Self) -> Self {
907        Self {
908            x: self.y * other.z - self.z * other.y,
909            y: self.z * other.x - self.x * other.z,
910            z: self.x * other.y - self.y * other.x,
911            units: UnitLen::Unknown,
912        }
913    }
914
915    pub fn normalize(&self) -> Self {
916        let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
917        Point3d {
918            x: self.x / len,
919            y: self.y / len,
920            z: self.z / len,
921            units: UnitLen::Unknown,
922        }
923    }
924}
925
926impl From<[TyF64; 3]> for Point3d {
927    fn from(p: [TyF64; 3]) -> Self {
928        Self {
929            x: p[0].n,
930            y: p[1].n,
931            z: p[2].n,
932            units: p[0].ty.expect_length(),
933        }
934    }
935}
936
937impl From<Point3d> for Point3D {
938    fn from(p: Point3d) -> Self {
939        Self { x: p.x, y: p.y, z: p.z }
940    }
941}
942impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
943    fn from(p: Point3d) -> Self {
944        Self {
945            x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
946            y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
947            z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
948        }
949    }
950}
951
952impl Add for Point3d {
953    type Output = Point3d;
954
955    fn add(self, rhs: Self) -> Self::Output {
956        // TODO should assert that self and rhs the same units or coerce them
957        Point3d {
958            x: self.x + rhs.x,
959            y: self.y + rhs.y,
960            z: self.z + rhs.z,
961            units: self.units,
962        }
963    }
964}
965
966impl AddAssign for Point3d {
967    fn add_assign(&mut self, rhs: Self) {
968        *self = *self + rhs
969    }
970}
971
972impl Mul<f64> for Point3d {
973    type Output = Point3d;
974
975    fn mul(self, rhs: f64) -> Self::Output {
976        Point3d {
977            x: self.x * rhs,
978            y: self.y * rhs,
979            z: self.z * rhs,
980            units: self.units,
981        }
982    }
983}
984
985/// A base path.
986#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
987#[ts(export)]
988#[serde(rename_all = "camelCase")]
989pub struct BasePath {
990    /// The from point.
991    #[ts(type = "[number, number]")]
992    pub from: [f64; 2],
993    /// The to point.
994    #[ts(type = "[number, number]")]
995    pub to: [f64; 2],
996    pub units: UnitLen,
997    /// The tag of the path.
998    pub tag: Option<TagNode>,
999    /// Metadata.
1000    #[serde(rename = "__geoMeta")]
1001    pub geo_meta: GeoMeta,
1002}
1003
1004impl BasePath {
1005    pub fn get_to(&self) -> [TyF64; 2] {
1006        let ty: NumericType = self.units.into();
1007        [TyF64::new(self.to[0], ty.clone()), TyF64::new(self.to[1], ty)]
1008    }
1009
1010    pub fn get_from(&self) -> [TyF64; 2] {
1011        let ty: NumericType = self.units.into();
1012        [TyF64::new(self.from[0], ty.clone()), TyF64::new(self.from[1], ty)]
1013    }
1014}
1015
1016/// Geometry metadata.
1017#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1018#[ts(export)]
1019#[serde(rename_all = "camelCase")]
1020pub struct GeoMeta {
1021    /// The id of the geometry.
1022    pub id: uuid::Uuid,
1023    /// Metadata.
1024    #[serde(flatten)]
1025    pub metadata: Metadata,
1026}
1027
1028/// A path.
1029#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1030#[ts(export)]
1031#[serde(tag = "type")]
1032pub enum Path {
1033    /// A path that goes to a point.
1034    ToPoint {
1035        #[serde(flatten)]
1036        base: BasePath,
1037    },
1038    /// A arc that is tangential to the last path segment that goes to a point
1039    TangentialArcTo {
1040        #[serde(flatten)]
1041        base: BasePath,
1042        /// the arc's center
1043        #[ts(type = "[number, number]")]
1044        center: [f64; 2],
1045        /// arc's direction
1046        ccw: bool,
1047    },
1048    /// A arc that is tangential to the last path segment
1049    TangentialArc {
1050        #[serde(flatten)]
1051        base: BasePath,
1052        /// the arc's center
1053        #[ts(type = "[number, number]")]
1054        center: [f64; 2],
1055        /// arc's direction
1056        ccw: bool,
1057    },
1058    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
1059    /// a complete arc
1060    Circle {
1061        #[serde(flatten)]
1062        base: BasePath,
1063        /// the arc's center
1064        #[ts(type = "[number, number]")]
1065        center: [f64; 2],
1066        /// the arc's radius
1067        radius: f64,
1068        /// arc's direction
1069        /// This is used to compute the tangential angle.
1070        ccw: bool,
1071    },
1072    CircleThreePoint {
1073        #[serde(flatten)]
1074        base: BasePath,
1075        /// Point 1 of the circle
1076        #[ts(type = "[number, number]")]
1077        p1: [f64; 2],
1078        /// Point 2 of the circle
1079        #[ts(type = "[number, number]")]
1080        p2: [f64; 2],
1081        /// Point 3 of the circle
1082        #[ts(type = "[number, number]")]
1083        p3: [f64; 2],
1084    },
1085    ArcThreePoint {
1086        #[serde(flatten)]
1087        base: BasePath,
1088        /// Point 1 of the arc (base on the end of previous segment)
1089        #[ts(type = "[number, number]")]
1090        p1: [f64; 2],
1091        /// Point 2 of the arc (interiorAbsolute kwarg)
1092        #[ts(type = "[number, number]")]
1093        p2: [f64; 2],
1094        /// Point 3 of the arc (endAbsolute kwarg)
1095        #[ts(type = "[number, number]")]
1096        p3: [f64; 2],
1097    },
1098    /// A path that is horizontal.
1099    Horizontal {
1100        #[serde(flatten)]
1101        base: BasePath,
1102        /// The x coordinate.
1103        x: f64,
1104    },
1105    /// An angled line to.
1106    AngledLineTo {
1107        #[serde(flatten)]
1108        base: BasePath,
1109        /// The x coordinate.
1110        x: Option<f64>,
1111        /// The y coordinate.
1112        y: Option<f64>,
1113    },
1114    /// A base path.
1115    Base {
1116        #[serde(flatten)]
1117        base: BasePath,
1118    },
1119    /// A circular arc, not necessarily tangential to the current point.
1120    Arc {
1121        #[serde(flatten)]
1122        base: BasePath,
1123        /// Center of the circle that this arc is drawn on.
1124        center: [f64; 2],
1125        /// Radius of the circle that this arc is drawn on.
1126        radius: f64,
1127        /// True if the arc is counterclockwise.
1128        ccw: bool,
1129    },
1130}
1131
1132/// What kind of path is this?
1133#[derive(Display)]
1134enum PathType {
1135    ToPoint,
1136    Base,
1137    TangentialArc,
1138    TangentialArcTo,
1139    Circle,
1140    CircleThreePoint,
1141    Horizontal,
1142    AngledLineTo,
1143    Arc,
1144}
1145
1146impl From<&Path> for PathType {
1147    fn from(value: &Path) -> Self {
1148        match value {
1149            Path::ToPoint { .. } => Self::ToPoint,
1150            Path::TangentialArcTo { .. } => Self::TangentialArcTo,
1151            Path::TangentialArc { .. } => Self::TangentialArc,
1152            Path::Circle { .. } => Self::Circle,
1153            Path::CircleThreePoint { .. } => Self::CircleThreePoint,
1154            Path::Horizontal { .. } => Self::Horizontal,
1155            Path::AngledLineTo { .. } => Self::AngledLineTo,
1156            Path::Base { .. } => Self::Base,
1157            Path::Arc { .. } => Self::Arc,
1158            Path::ArcThreePoint { .. } => Self::Arc,
1159        }
1160    }
1161}
1162
1163impl Path {
1164    pub fn get_id(&self) -> uuid::Uuid {
1165        match self {
1166            Path::ToPoint { base } => base.geo_meta.id,
1167            Path::Horizontal { base, .. } => base.geo_meta.id,
1168            Path::AngledLineTo { base, .. } => base.geo_meta.id,
1169            Path::Base { base } => base.geo_meta.id,
1170            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1171            Path::TangentialArc { base, .. } => base.geo_meta.id,
1172            Path::Circle { base, .. } => base.geo_meta.id,
1173            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1174            Path::Arc { base, .. } => base.geo_meta.id,
1175            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1176        }
1177    }
1178
1179    pub fn set_id(&mut self, id: uuid::Uuid) {
1180        match self {
1181            Path::ToPoint { base } => base.geo_meta.id = id,
1182            Path::Horizontal { base, .. } => base.geo_meta.id = id,
1183            Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1184            Path::Base { base } => base.geo_meta.id = id,
1185            Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1186            Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1187            Path::Circle { base, .. } => base.geo_meta.id = id,
1188            Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1189            Path::Arc { base, .. } => base.geo_meta.id = id,
1190            Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1191        }
1192    }
1193
1194    pub fn get_tag(&self) -> Option<TagNode> {
1195        match self {
1196            Path::ToPoint { base } => base.tag.clone(),
1197            Path::Horizontal { base, .. } => base.tag.clone(),
1198            Path::AngledLineTo { base, .. } => base.tag.clone(),
1199            Path::Base { base } => base.tag.clone(),
1200            Path::TangentialArcTo { base, .. } => base.tag.clone(),
1201            Path::TangentialArc { base, .. } => base.tag.clone(),
1202            Path::Circle { base, .. } => base.tag.clone(),
1203            Path::CircleThreePoint { base, .. } => base.tag.clone(),
1204            Path::Arc { base, .. } => base.tag.clone(),
1205            Path::ArcThreePoint { base, .. } => base.tag.clone(),
1206        }
1207    }
1208
1209    pub fn get_base(&self) -> &BasePath {
1210        match self {
1211            Path::ToPoint { base } => base,
1212            Path::Horizontal { base, .. } => base,
1213            Path::AngledLineTo { base, .. } => base,
1214            Path::Base { base } => base,
1215            Path::TangentialArcTo { base, .. } => base,
1216            Path::TangentialArc { base, .. } => base,
1217            Path::Circle { base, .. } => base,
1218            Path::CircleThreePoint { base, .. } => base,
1219            Path::Arc { base, .. } => base,
1220            Path::ArcThreePoint { base, .. } => base,
1221        }
1222    }
1223
1224    /// Where does this path segment start?
1225    pub fn get_from(&self) -> [TyF64; 2] {
1226        let p = &self.get_base().from;
1227        let ty: NumericType = self.get_base().units.into();
1228        [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1229    }
1230
1231    /// Where does this path segment end?
1232    pub fn get_to(&self) -> [TyF64; 2] {
1233        let p = &self.get_base().to;
1234        let ty: NumericType = self.get_base().units.into();
1235        [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1236    }
1237
1238    /// The path segment start point and its type.
1239    pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1240        let p = &self.get_base().from;
1241        let ty: NumericType = self.get_base().units.into();
1242        (*p, ty)
1243    }
1244
1245    /// The path segment end point and its type.
1246    pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1247        let p = &self.get_base().to;
1248        let ty: NumericType = self.get_base().units.into();
1249        (*p, ty)
1250    }
1251
1252    /// Length of this path segment, in cartesian plane.
1253    pub fn length(&self) -> TyF64 {
1254        let n = match self {
1255            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1256                linear_distance(&self.get_base().from, &self.get_base().to)
1257            }
1258            Self::TangentialArc {
1259                base: _,
1260                center,
1261                ccw: _,
1262            }
1263            | Self::TangentialArcTo {
1264                base: _,
1265                center,
1266                ccw: _,
1267            } => {
1268                // The radius can be calculated as the linear distance between `to` and `center`,
1269                // or between `from` and `center`. They should be the same.
1270                let radius = linear_distance(&self.get_base().from, center);
1271                debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1272                // TODO: Call engine utils to figure this out.
1273                linear_distance(&self.get_base().from, &self.get_base().to)
1274            }
1275            Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1276            Self::CircleThreePoint { .. } => {
1277                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1278                    self.get_base().from,
1279                    self.get_base().to,
1280                    self.get_base().to,
1281                ]);
1282                let radius = linear_distance(
1283                    &[circle_center.center[0], circle_center.center[1]],
1284                    &self.get_base().from,
1285                );
1286                2.0 * std::f64::consts::PI * radius
1287            }
1288            Self::Arc { .. } => {
1289                // TODO: Call engine utils to figure this out.
1290                linear_distance(&self.get_base().from, &self.get_base().to)
1291            }
1292            Self::ArcThreePoint { .. } => {
1293                // TODO: Call engine utils to figure this out.
1294                linear_distance(&self.get_base().from, &self.get_base().to)
1295            }
1296        };
1297        TyF64::new(n, self.get_base().units.into())
1298    }
1299
1300    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1301        match self {
1302            Path::ToPoint { base } => Some(base),
1303            Path::Horizontal { base, .. } => Some(base),
1304            Path::AngledLineTo { base, .. } => Some(base),
1305            Path::Base { base } => Some(base),
1306            Path::TangentialArcTo { base, .. } => Some(base),
1307            Path::TangentialArc { base, .. } => Some(base),
1308            Path::Circle { base, .. } => Some(base),
1309            Path::CircleThreePoint { base, .. } => Some(base),
1310            Path::Arc { base, .. } => Some(base),
1311            Path::ArcThreePoint { base, .. } => Some(base),
1312        }
1313    }
1314
1315    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1316        match self {
1317            Path::TangentialArc { center, ccw, .. }
1318            | Path::TangentialArcTo { center, ccw, .. }
1319            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1320                center: *center,
1321                ccw: *ccw,
1322            },
1323            Path::ArcThreePoint { p1, p2, p3, .. } => {
1324                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1325                GetTangentialInfoFromPathsResult::Arc {
1326                    center: circle.center,
1327                    ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1328                }
1329            }
1330            Path::Circle {
1331                center, ccw, radius, ..
1332            } => GetTangentialInfoFromPathsResult::Circle {
1333                center: *center,
1334                ccw: *ccw,
1335                radius: *radius,
1336            },
1337            Path::CircleThreePoint { p1, p2, p3, .. } => {
1338                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1339                let center_point = [circle.center[0], circle.center[1]];
1340                GetTangentialInfoFromPathsResult::Circle {
1341                    center: center_point,
1342                    // Note: a circle is always ccw regardless of the order of points
1343                    ccw: true,
1344                    radius: circle.radius,
1345                }
1346            }
1347            Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1348                let base = self.get_base();
1349                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1350            }
1351        }
1352    }
1353
1354    /// i.e. not a curve
1355    pub(crate) fn is_straight_line(&self) -> bool {
1356        matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1357    }
1358}
1359
1360/// Compute the straight-line distance between a pair of (2D) points.
1361#[rustfmt::skip]
1362fn linear_distance(
1363    [x0, y0]: &[f64; 2],
1364    [x1, y1]: &[f64; 2]
1365) -> f64 {
1366    let y_sq = (y1 - y0).powi(2);
1367    let x_sq = (x1 - x0).powi(2);
1368    (y_sq + x_sq).sqrt()
1369}
1370
1371/// An extrude surface.
1372#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1373#[ts(export)]
1374#[serde(tag = "type", rename_all = "camelCase")]
1375pub enum ExtrudeSurface {
1376    /// An extrude plane.
1377    ExtrudePlane(ExtrudePlane),
1378    ExtrudeArc(ExtrudeArc),
1379    Chamfer(ChamferSurface),
1380    Fillet(FilletSurface),
1381}
1382
1383// Chamfer surface.
1384#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1385#[ts(export)]
1386#[serde(rename_all = "camelCase")]
1387pub struct ChamferSurface {
1388    /// The id for the chamfer surface.
1389    pub face_id: uuid::Uuid,
1390    /// The tag.
1391    pub tag: Option<Node<TagDeclarator>>,
1392    /// Metadata.
1393    #[serde(flatten)]
1394    pub geo_meta: GeoMeta,
1395}
1396
1397// Fillet surface.
1398#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1399#[ts(export)]
1400#[serde(rename_all = "camelCase")]
1401pub struct FilletSurface {
1402    /// The id for the fillet surface.
1403    pub face_id: uuid::Uuid,
1404    /// The tag.
1405    pub tag: Option<Node<TagDeclarator>>,
1406    /// Metadata.
1407    #[serde(flatten)]
1408    pub geo_meta: GeoMeta,
1409}
1410
1411/// An extruded plane.
1412#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1413#[ts(export)]
1414#[serde(rename_all = "camelCase")]
1415pub struct ExtrudePlane {
1416    /// The face id for the extrude plane.
1417    pub face_id: uuid::Uuid,
1418    /// The tag.
1419    pub tag: Option<Node<TagDeclarator>>,
1420    /// Metadata.
1421    #[serde(flatten)]
1422    pub geo_meta: GeoMeta,
1423}
1424
1425/// An extruded arc.
1426#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1427#[ts(export)]
1428#[serde(rename_all = "camelCase")]
1429pub struct ExtrudeArc {
1430    /// The face id for the extrude plane.
1431    pub face_id: uuid::Uuid,
1432    /// The tag.
1433    pub tag: Option<Node<TagDeclarator>>,
1434    /// Metadata.
1435    #[serde(flatten)]
1436    pub geo_meta: GeoMeta,
1437}
1438
1439impl ExtrudeSurface {
1440    pub fn get_id(&self) -> uuid::Uuid {
1441        match self {
1442            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1443            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1444            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1445            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1446        }
1447    }
1448
1449    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1450        match self {
1451            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1452            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1453            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1454            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1455        }
1456    }
1457}