kcl_lib/execution/
geometry.rs

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