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::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq};
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    engine::{DEFAULT_PLANE_INFO, PlaneName},
13    errors::{KclError, KclErrorDetails},
14    execution::{
15        ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen, types::NumericType,
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 {value:?} not found"),
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 {name} not found"),
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: i, .. } => *i = id,
819            EdgeCut::Chamfer { id: 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: i, .. } => *i = id,
833            EdgeCut::Chamfer { edge_id: 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}
942
943impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
944    fn from(p: Point3d) -> Self {
945        Self {
946            x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
947            y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
948            z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
949        }
950    }
951}
952
953impl Add for Point3d {
954    type Output = Point3d;
955
956    fn add(self, rhs: Self) -> Self::Output {
957        // TODO should assert that self and rhs the same units or coerce them
958        Point3d {
959            x: self.x + rhs.x,
960            y: self.y + rhs.y,
961            z: self.z + rhs.z,
962            units: self.units,
963        }
964    }
965}
966
967impl AddAssign for Point3d {
968    fn add_assign(&mut self, rhs: Self) {
969        *self = *self + rhs
970    }
971}
972
973impl Mul<f64> for Point3d {
974    type Output = Point3d;
975
976    fn mul(self, rhs: f64) -> Self::Output {
977        Point3d {
978            x: self.x * rhs,
979            y: self.y * rhs,
980            z: self.z * rhs,
981            units: self.units,
982        }
983    }
984}
985
986/// A base path.
987#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
988#[ts(export)]
989#[serde(rename_all = "camelCase")]
990pub struct BasePath {
991    /// The from point.
992    #[ts(type = "[number, number]")]
993    pub from: [f64; 2],
994    /// The to point.
995    #[ts(type = "[number, number]")]
996    pub to: [f64; 2],
997    pub units: UnitLen,
998    /// The tag of the path.
999    pub tag: Option<TagNode>,
1000    /// Metadata.
1001    #[serde(rename = "__geoMeta")]
1002    pub geo_meta: GeoMeta,
1003}
1004
1005impl BasePath {
1006    pub fn get_to(&self) -> [TyF64; 2] {
1007        let ty: NumericType = self.units.into();
1008        [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1009    }
1010
1011    pub fn get_from(&self) -> [TyF64; 2] {
1012        let ty: NumericType = self.units.into();
1013        [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1014    }
1015}
1016
1017/// Geometry metadata.
1018#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1019#[ts(export)]
1020#[serde(rename_all = "camelCase")]
1021pub struct GeoMeta {
1022    /// The id of the geometry.
1023    pub id: uuid::Uuid,
1024    /// Metadata.
1025    #[serde(flatten)]
1026    pub metadata: Metadata,
1027}
1028
1029/// A path.
1030#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1031#[ts(export)]
1032#[serde(tag = "type")]
1033pub enum Path {
1034    /// A straight line which ends at the given point.
1035    ToPoint {
1036        #[serde(flatten)]
1037        base: BasePath,
1038    },
1039    /// A arc that is tangential to the last path segment that goes to a point
1040    TangentialArcTo {
1041        #[serde(flatten)]
1042        base: BasePath,
1043        /// the arc's center
1044        #[ts(type = "[number, number]")]
1045        center: [f64; 2],
1046        /// arc's direction
1047        ccw: bool,
1048    },
1049    /// A arc that is tangential to the last path segment
1050    TangentialArc {
1051        #[serde(flatten)]
1052        base: BasePath,
1053        /// the arc's center
1054        #[ts(type = "[number, number]")]
1055        center: [f64; 2],
1056        /// arc's direction
1057        ccw: bool,
1058    },
1059    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
1060    /// a complete arc
1061    Circle {
1062        #[serde(flatten)]
1063        base: BasePath,
1064        /// the arc's center
1065        #[ts(type = "[number, number]")]
1066        center: [f64; 2],
1067        /// the arc's radius
1068        radius: f64,
1069        /// arc's direction
1070        /// This is used to compute the tangential angle.
1071        ccw: bool,
1072    },
1073    CircleThreePoint {
1074        #[serde(flatten)]
1075        base: BasePath,
1076        /// Point 1 of the circle
1077        #[ts(type = "[number, number]")]
1078        p1: [f64; 2],
1079        /// Point 2 of the circle
1080        #[ts(type = "[number, number]")]
1081        p2: [f64; 2],
1082        /// Point 3 of the circle
1083        #[ts(type = "[number, number]")]
1084        p3: [f64; 2],
1085    },
1086    ArcThreePoint {
1087        #[serde(flatten)]
1088        base: BasePath,
1089        /// Point 1 of the arc (base on the end of previous segment)
1090        #[ts(type = "[number, number]")]
1091        p1: [f64; 2],
1092        /// Point 2 of the arc (interiorAbsolute kwarg)
1093        #[ts(type = "[number, number]")]
1094        p2: [f64; 2],
1095        /// Point 3 of the arc (endAbsolute kwarg)
1096        #[ts(type = "[number, number]")]
1097        p3: [f64; 2],
1098    },
1099    /// A path that is horizontal.
1100    Horizontal {
1101        #[serde(flatten)]
1102        base: BasePath,
1103        /// The x coordinate.
1104        x: f64,
1105    },
1106    /// An angled line to.
1107    AngledLineTo {
1108        #[serde(flatten)]
1109        base: BasePath,
1110        /// The x coordinate.
1111        x: Option<f64>,
1112        /// The y coordinate.
1113        y: Option<f64>,
1114    },
1115    /// A base path.
1116    Base {
1117        #[serde(flatten)]
1118        base: BasePath,
1119    },
1120    /// A circular arc, not necessarily tangential to the current point.
1121    Arc {
1122        #[serde(flatten)]
1123        base: BasePath,
1124        /// Center of the circle that this arc is drawn on.
1125        center: [f64; 2],
1126        /// Radius of the circle that this arc is drawn on.
1127        radius: f64,
1128        /// True if the arc is counterclockwise.
1129        ccw: bool,
1130    },
1131}
1132
1133/// What kind of path is this?
1134#[derive(Display)]
1135enum PathType {
1136    ToPoint,
1137    Base,
1138    TangentialArc,
1139    TangentialArcTo,
1140    Circle,
1141    CircleThreePoint,
1142    Horizontal,
1143    AngledLineTo,
1144    Arc,
1145}
1146
1147impl From<&Path> for PathType {
1148    fn from(value: &Path) -> Self {
1149        match value {
1150            Path::ToPoint { .. } => Self::ToPoint,
1151            Path::TangentialArcTo { .. } => Self::TangentialArcTo,
1152            Path::TangentialArc { .. } => Self::TangentialArc,
1153            Path::Circle { .. } => Self::Circle,
1154            Path::CircleThreePoint { .. } => Self::CircleThreePoint,
1155            Path::Horizontal { .. } => Self::Horizontal,
1156            Path::AngledLineTo { .. } => Self::AngledLineTo,
1157            Path::Base { .. } => Self::Base,
1158            Path::Arc { .. } => Self::Arc,
1159            Path::ArcThreePoint { .. } => Self::Arc,
1160        }
1161    }
1162}
1163
1164impl Path {
1165    pub fn get_id(&self) -> uuid::Uuid {
1166        match self {
1167            Path::ToPoint { base } => base.geo_meta.id,
1168            Path::Horizontal { base, .. } => base.geo_meta.id,
1169            Path::AngledLineTo { base, .. } => base.geo_meta.id,
1170            Path::Base { base } => base.geo_meta.id,
1171            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1172            Path::TangentialArc { base, .. } => base.geo_meta.id,
1173            Path::Circle { base, .. } => base.geo_meta.id,
1174            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1175            Path::Arc { base, .. } => base.geo_meta.id,
1176            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1177        }
1178    }
1179
1180    pub fn set_id(&mut self, id: uuid::Uuid) {
1181        match self {
1182            Path::ToPoint { base } => base.geo_meta.id = id,
1183            Path::Horizontal { base, .. } => base.geo_meta.id = id,
1184            Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1185            Path::Base { base } => base.geo_meta.id = id,
1186            Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1187            Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1188            Path::Circle { base, .. } => base.geo_meta.id = id,
1189            Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1190            Path::Arc { base, .. } => base.geo_meta.id = id,
1191            Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1192        }
1193    }
1194
1195    pub fn get_tag(&self) -> Option<TagNode> {
1196        match self {
1197            Path::ToPoint { base } => base.tag.clone(),
1198            Path::Horizontal { base, .. } => base.tag.clone(),
1199            Path::AngledLineTo { base, .. } => base.tag.clone(),
1200            Path::Base { base } => base.tag.clone(),
1201            Path::TangentialArcTo { base, .. } => base.tag.clone(),
1202            Path::TangentialArc { base, .. } => base.tag.clone(),
1203            Path::Circle { base, .. } => base.tag.clone(),
1204            Path::CircleThreePoint { base, .. } => base.tag.clone(),
1205            Path::Arc { base, .. } => base.tag.clone(),
1206            Path::ArcThreePoint { base, .. } => base.tag.clone(),
1207        }
1208    }
1209
1210    pub fn get_base(&self) -> &BasePath {
1211        match self {
1212            Path::ToPoint { base } => base,
1213            Path::Horizontal { base, .. } => base,
1214            Path::AngledLineTo { base, .. } => base,
1215            Path::Base { base } => base,
1216            Path::TangentialArcTo { base, .. } => base,
1217            Path::TangentialArc { base, .. } => base,
1218            Path::Circle { base, .. } => base,
1219            Path::CircleThreePoint { base, .. } => base,
1220            Path::Arc { base, .. } => base,
1221            Path::ArcThreePoint { base, .. } => base,
1222        }
1223    }
1224
1225    /// Where does this path segment start?
1226    pub fn get_from(&self) -> [TyF64; 2] {
1227        let p = &self.get_base().from;
1228        let ty: NumericType = self.get_base().units.into();
1229        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1230    }
1231
1232    /// Where does this path segment end?
1233    pub fn get_to(&self) -> [TyF64; 2] {
1234        let p = &self.get_base().to;
1235        let ty: NumericType = self.get_base().units.into();
1236        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1237    }
1238
1239    /// The path segment start point and its type.
1240    pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1241        let p = &self.get_base().from;
1242        let ty: NumericType = self.get_base().units.into();
1243        (*p, ty)
1244    }
1245
1246    /// The path segment end point and its type.
1247    pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1248        let p = &self.get_base().to;
1249        let ty: NumericType = self.get_base().units.into();
1250        (*p, ty)
1251    }
1252
1253    /// Length of this path segment, in cartesian plane.
1254    pub fn length(&self) -> TyF64 {
1255        let n = match self {
1256            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1257                linear_distance(&self.get_base().from, &self.get_base().to)
1258            }
1259            Self::TangentialArc {
1260                base: _,
1261                center,
1262                ccw: _,
1263            }
1264            | Self::TangentialArcTo {
1265                base: _,
1266                center,
1267                ccw: _,
1268            } => {
1269                // The radius can be calculated as the linear distance between `to` and `center`,
1270                // or between `from` and `center`. They should be the same.
1271                let radius = linear_distance(&self.get_base().from, center);
1272                debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1273                // TODO: Call engine utils to figure this out.
1274                linear_distance(&self.get_base().from, &self.get_base().to)
1275            }
1276            Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1277            Self::CircleThreePoint { .. } => {
1278                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1279                    self.get_base().from,
1280                    self.get_base().to,
1281                    self.get_base().to,
1282                ]);
1283                let radius = linear_distance(
1284                    &[circle_center.center[0], circle_center.center[1]],
1285                    &self.get_base().from,
1286                );
1287                2.0 * std::f64::consts::PI * radius
1288            }
1289            Self::Arc { .. } => {
1290                // TODO: Call engine utils to figure this out.
1291                linear_distance(&self.get_base().from, &self.get_base().to)
1292            }
1293            Self::ArcThreePoint { .. } => {
1294                // TODO: Call engine utils to figure this out.
1295                linear_distance(&self.get_base().from, &self.get_base().to)
1296            }
1297        };
1298        TyF64::new(n, self.get_base().units.into())
1299    }
1300
1301    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1302        match self {
1303            Path::ToPoint { base } => Some(base),
1304            Path::Horizontal { base, .. } => Some(base),
1305            Path::AngledLineTo { base, .. } => Some(base),
1306            Path::Base { base } => Some(base),
1307            Path::TangentialArcTo { base, .. } => Some(base),
1308            Path::TangentialArc { base, .. } => Some(base),
1309            Path::Circle { base, .. } => Some(base),
1310            Path::CircleThreePoint { base, .. } => Some(base),
1311            Path::Arc { base, .. } => Some(base),
1312            Path::ArcThreePoint { base, .. } => Some(base),
1313        }
1314    }
1315
1316    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1317        match self {
1318            Path::TangentialArc { center, ccw, .. }
1319            | Path::TangentialArcTo { center, ccw, .. }
1320            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1321                center: *center,
1322                ccw: *ccw,
1323            },
1324            Path::ArcThreePoint { p1, p2, p3, .. } => {
1325                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1326                GetTangentialInfoFromPathsResult::Arc {
1327                    center: circle.center,
1328                    ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1329                }
1330            }
1331            Path::Circle {
1332                center, ccw, radius, ..
1333            } => GetTangentialInfoFromPathsResult::Circle {
1334                center: *center,
1335                ccw: *ccw,
1336                radius: *radius,
1337            },
1338            Path::CircleThreePoint { p1, p2, p3, .. } => {
1339                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1340                let center_point = [circle.center[0], circle.center[1]];
1341                GetTangentialInfoFromPathsResult::Circle {
1342                    center: center_point,
1343                    // Note: a circle is always ccw regardless of the order of points
1344                    ccw: true,
1345                    radius: circle.radius,
1346                }
1347            }
1348            Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1349                let base = self.get_base();
1350                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1351            }
1352        }
1353    }
1354
1355    /// i.e. not a curve
1356    pub(crate) fn is_straight_line(&self) -> bool {
1357        matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1358    }
1359}
1360
1361/// Compute the straight-line distance between a pair of (2D) points.
1362#[rustfmt::skip]
1363fn linear_distance(
1364    [x0, y0]: &[f64; 2],
1365    [x1, y1]: &[f64; 2]
1366) -> f64 {
1367    let y_sq = (y1 - y0).powi(2);
1368    let x_sq = (x1 - x0).powi(2);
1369    (y_sq + x_sq).sqrt()
1370}
1371
1372/// An extrude surface.
1373#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1374#[ts(export)]
1375#[serde(tag = "type", rename_all = "camelCase")]
1376pub enum ExtrudeSurface {
1377    /// An extrude plane.
1378    ExtrudePlane(ExtrudePlane),
1379    ExtrudeArc(ExtrudeArc),
1380    Chamfer(ChamferSurface),
1381    Fillet(FilletSurface),
1382}
1383
1384// Chamfer surface.
1385#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1386#[ts(export)]
1387#[serde(rename_all = "camelCase")]
1388pub struct ChamferSurface {
1389    /// The id for the chamfer surface.
1390    pub face_id: uuid::Uuid,
1391    /// The tag.
1392    pub tag: Option<Node<TagDeclarator>>,
1393    /// Metadata.
1394    #[serde(flatten)]
1395    pub geo_meta: GeoMeta,
1396}
1397
1398// Fillet surface.
1399#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1400#[ts(export)]
1401#[serde(rename_all = "camelCase")]
1402pub struct FilletSurface {
1403    /// The id for the fillet surface.
1404    pub face_id: uuid::Uuid,
1405    /// The tag.
1406    pub tag: Option<Node<TagDeclarator>>,
1407    /// Metadata.
1408    #[serde(flatten)]
1409    pub geo_meta: GeoMeta,
1410}
1411
1412/// An extruded plane.
1413#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1414#[ts(export)]
1415#[serde(rename_all = "camelCase")]
1416pub struct ExtrudePlane {
1417    /// The face id for the extrude plane.
1418    pub face_id: uuid::Uuid,
1419    /// The tag.
1420    pub tag: Option<Node<TagDeclarator>>,
1421    /// Metadata.
1422    #[serde(flatten)]
1423    pub geo_meta: GeoMeta,
1424}
1425
1426/// An extruded arc.
1427#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1428#[ts(export)]
1429#[serde(rename_all = "camelCase")]
1430pub struct ExtrudeArc {
1431    /// The face id for the extrude plane.
1432    pub face_id: uuid::Uuid,
1433    /// The tag.
1434    pub tag: Option<Node<TagDeclarator>>,
1435    /// Metadata.
1436    #[serde(flatten)]
1437    pub geo_meta: GeoMeta,
1438}
1439
1440impl ExtrudeSurface {
1441    pub fn get_id(&self) -> uuid::Uuid {
1442        match self {
1443            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1444            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1445            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1446            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1447        }
1448    }
1449
1450    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1451        match self {
1452            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1453            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1454            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1455            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1456        }
1457    }
1458}