kcl_lib/execution/
geometry.rs

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