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