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