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