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(
772        &mut self,
773        tag: NodeRef<'_, TagDeclarator>,
774        current_path: &Path,
775        exec_state: &ExecState,
776        surface: Option<&ExtrudeSurface>,
777    ) {
778        let mut tag_identifier: TagIdentifier = tag.into();
779        let base = current_path.get_base();
780        tag_identifier.info.push((
781            exec_state.stack().current_epoch(),
782            TagEngineInfo {
783                id: base.geo_meta.id,
784                sketch: self.id,
785                path: Some(current_path.clone()),
786                surface: surface.cloned(),
787            },
788        ));
789
790        self.tags.insert(tag.name.to_string(), tag_identifier);
791    }
792
793    pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
794        for t in tags {
795            match self.tags.get_mut(&t.value) {
796                Some(id) => {
797                    id.merge_info(t);
798                }
799                None => {
800                    self.tags.insert(t.value.clone(), t.clone());
801                }
802            }
803        }
804    }
805
806    /// Get the path most recently sketched.
807    pub(crate) fn latest_path(&self) -> Option<&Path> {
808        self.paths.last()
809    }
810
811    /// The "pen" is an imaginary pen drawing the path.
812    /// This gets the current point the pen is hovering over, i.e. the point
813    /// where the last path segment ends, and the next path segment will begin.
814    pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
815        let Some(path) = self.latest_path() else {
816            return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
817        };
818
819        let to = path.get_base().to;
820        Ok(Point2d::new(to[0], to[1], path.get_base().units))
821    }
822
823    pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
824        let Some(path) = self.latest_path() else {
825            return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
826        };
827        path.get_tangential_info()
828    }
829}
830
831#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
832#[ts(export)]
833#[serde(tag = "type", rename_all = "camelCase")]
834pub struct Solid {
835    /// The id of the solid.
836    pub id: uuid::Uuid,
837    /// The artifact ID of the solid.  Unlike `id`, this doesn't change.
838    pub artifact_id: ArtifactId,
839    /// The extrude surfaces.
840    pub value: Vec<ExtrudeSurface>,
841    /// The sketch.
842    pub sketch: Sketch,
843    /// The id of the extrusion start cap
844    pub start_cap_id: Option<uuid::Uuid>,
845    /// The id of the extrusion end cap
846    pub end_cap_id: Option<uuid::Uuid>,
847    /// Chamfers or fillets on this solid.
848    #[serde(default, skip_serializing_if = "Vec::is_empty")]
849    pub edge_cuts: Vec<EdgeCut>,
850    /// The units of the solid.
851    pub units: UnitLength,
852    /// Is this a sectional solid?
853    pub sectional: bool,
854    /// Metadata.
855    #[serde(skip)]
856    pub meta: Vec<Metadata>,
857}
858
859impl Solid {
860    pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
861        self.edge_cuts.iter().map(|foc| foc.id())
862    }
863}
864
865/// A fillet or a chamfer.
866#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
867#[ts(export)]
868#[serde(tag = "type", rename_all = "camelCase")]
869pub enum EdgeCut {
870    /// A fillet.
871    Fillet {
872        /// The id of the engine command that called this fillet.
873        id: uuid::Uuid,
874        radius: TyF64,
875        /// The engine id of the edge to fillet.
876        #[serde(rename = "edgeId")]
877        edge_id: uuid::Uuid,
878        tag: Box<Option<TagNode>>,
879    },
880    /// A chamfer.
881    Chamfer {
882        /// The id of the engine command that called this chamfer.
883        id: uuid::Uuid,
884        length: TyF64,
885        /// The engine id of the edge to chamfer.
886        #[serde(rename = "edgeId")]
887        edge_id: uuid::Uuid,
888        tag: Box<Option<TagNode>>,
889    },
890}
891
892impl EdgeCut {
893    pub fn id(&self) -> uuid::Uuid {
894        match self {
895            EdgeCut::Fillet { id, .. } => *id,
896            EdgeCut::Chamfer { id, .. } => *id,
897        }
898    }
899
900    pub fn set_id(&mut self, id: uuid::Uuid) {
901        match self {
902            EdgeCut::Fillet { id: i, .. } => *i = id,
903            EdgeCut::Chamfer { id: i, .. } => *i = id,
904        }
905    }
906
907    pub fn edge_id(&self) -> uuid::Uuid {
908        match self {
909            EdgeCut::Fillet { edge_id, .. } => *edge_id,
910            EdgeCut::Chamfer { edge_id, .. } => *edge_id,
911        }
912    }
913
914    pub fn set_edge_id(&mut self, id: uuid::Uuid) {
915        match self {
916            EdgeCut::Fillet { edge_id: i, .. } => *i = id,
917            EdgeCut::Chamfer { edge_id: i, .. } => *i = id,
918        }
919    }
920
921    pub fn tag(&self) -> Option<TagNode> {
922        match self {
923            EdgeCut::Fillet { tag, .. } => *tag.clone(),
924            EdgeCut::Chamfer { tag, .. } => *tag.clone(),
925        }
926    }
927}
928
929#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS)]
930#[ts(export)]
931pub struct Point2d {
932    pub x: f64,
933    pub y: f64,
934    pub units: UnitLength,
935}
936
937impl Point2d {
938    pub const ZERO: Self = Self {
939        x: 0.0,
940        y: 0.0,
941        units: UnitLength::Millimeters,
942    };
943
944    pub fn new(x: f64, y: f64, units: UnitLength) -> Self {
945        Self { x, y, units }
946    }
947
948    pub fn into_x(self) -> TyF64 {
949        TyF64::new(self.x, self.units.into())
950    }
951
952    pub fn into_y(self) -> TyF64 {
953        TyF64::new(self.y, self.units.into())
954    }
955
956    pub fn ignore_units(self) -> [f64; 2] {
957        [self.x, self.y]
958    }
959}
960
961#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, Default)]
962#[ts(export)]
963pub struct Point3d {
964    pub x: f64,
965    pub y: f64,
966    pub z: f64,
967    pub units: Option<UnitLength>,
968}
969
970impl Point3d {
971    pub const ZERO: Self = Self {
972        x: 0.0,
973        y: 0.0,
974        z: 0.0,
975        units: Some(UnitLength::Millimeters),
976    };
977
978    pub fn new(x: f64, y: f64, z: f64, units: Option<UnitLength>) -> Self {
979        Self { x, y, z, units }
980    }
981
982    pub const fn is_zero(&self) -> bool {
983        self.x == 0.0 && self.y == 0.0 && self.z == 0.0
984    }
985
986    /// Calculate the cross product of this vector with another.
987    ///
988    /// This should only be applied to axes or other vectors which represent only a direction (and
989    /// no magnitude) since units are ignored.
990    pub fn axes_cross_product(&self, other: &Self) -> Self {
991        Self {
992            x: self.y * other.z - self.z * other.y,
993            y: self.z * other.x - self.x * other.z,
994            z: self.x * other.y - self.y * other.x,
995            units: None,
996        }
997    }
998
999    /// Calculate the dot product of this vector with another.
1000    ///
1001    /// This should only be applied to axes or other vectors which represent only a direction (and
1002    /// no magnitude) since units are ignored.
1003    pub fn axes_dot_product(&self, other: &Self) -> f64 {
1004        let x = self.x * other.x;
1005        let y = self.y * other.y;
1006        let z = self.z * other.z;
1007        x + y + z
1008    }
1009
1010    pub fn normalize(&self) -> Self {
1011        let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
1012        Point3d {
1013            x: self.x / len,
1014            y: self.y / len,
1015            z: self.z / len,
1016            units: None,
1017        }
1018    }
1019
1020    pub fn as_3_dims(&self) -> ([f64; 3], Option<UnitLength>) {
1021        let p = [self.x, self.y, self.z];
1022        let u = self.units;
1023        (p, u)
1024    }
1025
1026    pub(crate) fn negated(self) -> Self {
1027        Self {
1028            x: -self.x,
1029            y: -self.y,
1030            z: -self.z,
1031            units: self.units,
1032        }
1033    }
1034}
1035
1036impl From<[TyF64; 3]> for Point3d {
1037    fn from(p: [TyF64; 3]) -> Self {
1038        Self {
1039            x: p[0].n,
1040            y: p[1].n,
1041            z: p[2].n,
1042            units: p[0].ty.as_length(),
1043        }
1044    }
1045}
1046
1047impl From<Point3d> for Point3D {
1048    fn from(p: Point3d) -> Self {
1049        Self { x: p.x, y: p.y, z: p.z }
1050    }
1051}
1052
1053impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
1054    fn from(p: Point3d) -> Self {
1055        if let Some(units) = p.units {
1056            Self {
1057                x: LengthUnit(adjust_length(units, p.x, UnitLength::Millimeters).0),
1058                y: LengthUnit(adjust_length(units, p.y, UnitLength::Millimeters).0),
1059                z: LengthUnit(adjust_length(units, p.z, UnitLength::Millimeters).0),
1060            }
1061        } else {
1062            Self {
1063                x: LengthUnit(p.x),
1064                y: LengthUnit(p.y),
1065                z: LengthUnit(p.z),
1066            }
1067        }
1068    }
1069}
1070
1071impl Add for Point3d {
1072    type Output = Point3d;
1073
1074    fn add(self, rhs: Self) -> Self::Output {
1075        // TODO should assert that self and rhs the same units or coerce them
1076        Point3d {
1077            x: self.x + rhs.x,
1078            y: self.y + rhs.y,
1079            z: self.z + rhs.z,
1080            units: self.units,
1081        }
1082    }
1083}
1084
1085impl AddAssign for Point3d {
1086    fn add_assign(&mut self, rhs: Self) {
1087        *self = *self + rhs
1088    }
1089}
1090
1091impl Sub for Point3d {
1092    type Output = Point3d;
1093
1094    fn sub(self, rhs: Self) -> Self::Output {
1095        let (x, y, z) = if rhs.units != self.units
1096            && let Some(sunits) = self.units
1097            && let Some(runits) = rhs.units
1098        {
1099            (
1100                adjust_length(runits, rhs.x, sunits).0,
1101                adjust_length(runits, rhs.y, sunits).0,
1102                adjust_length(runits, rhs.z, sunits).0,
1103            )
1104        } else {
1105            (rhs.x, rhs.y, rhs.z)
1106        };
1107        Point3d {
1108            x: self.x - x,
1109            y: self.y - y,
1110            z: self.z - z,
1111            units: self.units,
1112        }
1113    }
1114}
1115
1116impl SubAssign for Point3d {
1117    fn sub_assign(&mut self, rhs: Self) {
1118        *self = *self - rhs
1119    }
1120}
1121
1122impl Mul<f64> for Point3d {
1123    type Output = Point3d;
1124
1125    fn mul(self, rhs: f64) -> Self::Output {
1126        Point3d {
1127            x: self.x * rhs,
1128            y: self.y * rhs,
1129            z: self.z * rhs,
1130            units: self.units,
1131        }
1132    }
1133}
1134
1135/// A base path.
1136#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1137#[ts(export)]
1138#[serde(rename_all = "camelCase")]
1139pub struct BasePath {
1140    /// The from point.
1141    #[ts(type = "[number, number]")]
1142    pub from: [f64; 2],
1143    /// The to point.
1144    #[ts(type = "[number, number]")]
1145    pub to: [f64; 2],
1146    pub units: UnitLength,
1147    /// The tag of the path.
1148    pub tag: Option<TagNode>,
1149    /// Metadata.
1150    #[serde(rename = "__geoMeta")]
1151    pub geo_meta: GeoMeta,
1152}
1153
1154impl BasePath {
1155    pub fn get_to(&self) -> [TyF64; 2] {
1156        let ty: NumericType = self.units.into();
1157        [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1158    }
1159
1160    pub fn get_from(&self) -> [TyF64; 2] {
1161        let ty: NumericType = self.units.into();
1162        [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1163    }
1164}
1165
1166/// Geometry metadata.
1167#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1168#[ts(export)]
1169#[serde(rename_all = "camelCase")]
1170pub struct GeoMeta {
1171    /// The id of the geometry.
1172    pub id: uuid::Uuid,
1173    /// Metadata.
1174    #[serde(flatten)]
1175    pub metadata: Metadata,
1176}
1177
1178/// A path.
1179#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1180#[ts(export)]
1181#[serde(tag = "type")]
1182pub enum Path {
1183    /// A straight line which ends at the given point.
1184    ToPoint {
1185        #[serde(flatten)]
1186        base: BasePath,
1187    },
1188    /// A arc that is tangential to the last path segment that goes to a point
1189    TangentialArcTo {
1190        #[serde(flatten)]
1191        base: BasePath,
1192        /// the arc's center
1193        #[ts(type = "[number, number]")]
1194        center: [f64; 2],
1195        /// arc's direction
1196        ccw: bool,
1197    },
1198    /// A arc that is tangential to the last path segment
1199    TangentialArc {
1200        #[serde(flatten)]
1201        base: BasePath,
1202        /// the arc's center
1203        #[ts(type = "[number, number]")]
1204        center: [f64; 2],
1205        /// arc's direction
1206        ccw: bool,
1207    },
1208    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
1209    /// a complete arc
1210    Circle {
1211        #[serde(flatten)]
1212        base: BasePath,
1213        /// the arc's center
1214        #[ts(type = "[number, number]")]
1215        center: [f64; 2],
1216        /// the arc's radius
1217        radius: f64,
1218        /// arc's direction
1219        /// This is used to compute the tangential angle.
1220        ccw: bool,
1221    },
1222    CircleThreePoint {
1223        #[serde(flatten)]
1224        base: BasePath,
1225        /// Point 1 of the circle
1226        #[ts(type = "[number, number]")]
1227        p1: [f64; 2],
1228        /// Point 2 of the circle
1229        #[ts(type = "[number, number]")]
1230        p2: [f64; 2],
1231        /// Point 3 of the circle
1232        #[ts(type = "[number, number]")]
1233        p3: [f64; 2],
1234    },
1235    ArcThreePoint {
1236        #[serde(flatten)]
1237        base: BasePath,
1238        /// Point 1 of the arc (base on the end of previous segment)
1239        #[ts(type = "[number, number]")]
1240        p1: [f64; 2],
1241        /// Point 2 of the arc (interiorAbsolute kwarg)
1242        #[ts(type = "[number, number]")]
1243        p2: [f64; 2],
1244        /// Point 3 of the arc (endAbsolute kwarg)
1245        #[ts(type = "[number, number]")]
1246        p3: [f64; 2],
1247    },
1248    /// A path that is horizontal.
1249    Horizontal {
1250        #[serde(flatten)]
1251        base: BasePath,
1252        /// The x coordinate.
1253        x: f64,
1254    },
1255    /// An angled line to.
1256    AngledLineTo {
1257        #[serde(flatten)]
1258        base: BasePath,
1259        /// The x coordinate.
1260        x: Option<f64>,
1261        /// The y coordinate.
1262        y: Option<f64>,
1263    },
1264    /// A base path.
1265    Base {
1266        #[serde(flatten)]
1267        base: BasePath,
1268    },
1269    /// A circular arc, not necessarily tangential to the current point.
1270    Arc {
1271        #[serde(flatten)]
1272        base: BasePath,
1273        /// Center of the circle that this arc is drawn on.
1274        center: [f64; 2],
1275        /// Radius of the circle that this arc is drawn on.
1276        radius: f64,
1277        /// True if the arc is counterclockwise.
1278        ccw: bool,
1279    },
1280    Ellipse {
1281        #[serde(flatten)]
1282        base: BasePath,
1283        center: [f64; 2],
1284        major_axis: [f64; 2],
1285        minor_radius: f64,
1286        ccw: bool,
1287    },
1288    //TODO: (bc) figure this out
1289    Conic {
1290        #[serde(flatten)]
1291        base: BasePath,
1292    },
1293}
1294
1295impl Path {
1296    pub fn get_id(&self) -> uuid::Uuid {
1297        match self {
1298            Path::ToPoint { base } => base.geo_meta.id,
1299            Path::Horizontal { base, .. } => base.geo_meta.id,
1300            Path::AngledLineTo { base, .. } => base.geo_meta.id,
1301            Path::Base { base } => base.geo_meta.id,
1302            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1303            Path::TangentialArc { base, .. } => base.geo_meta.id,
1304            Path::Circle { base, .. } => base.geo_meta.id,
1305            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1306            Path::Arc { base, .. } => base.geo_meta.id,
1307            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1308            Path::Ellipse { base, .. } => base.geo_meta.id,
1309            Path::Conic { base, .. } => base.geo_meta.id,
1310        }
1311    }
1312
1313    pub fn set_id(&mut self, id: uuid::Uuid) {
1314        match self {
1315            Path::ToPoint { base } => base.geo_meta.id = id,
1316            Path::Horizontal { base, .. } => base.geo_meta.id = id,
1317            Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1318            Path::Base { base } => base.geo_meta.id = id,
1319            Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1320            Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1321            Path::Circle { base, .. } => base.geo_meta.id = id,
1322            Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1323            Path::Arc { base, .. } => base.geo_meta.id = id,
1324            Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1325            Path::Ellipse { base, .. } => base.geo_meta.id = id,
1326            Path::Conic { base, .. } => base.geo_meta.id = id,
1327        }
1328    }
1329
1330    pub fn get_tag(&self) -> Option<TagNode> {
1331        match self {
1332            Path::ToPoint { base } => base.tag.clone(),
1333            Path::Horizontal { base, .. } => base.tag.clone(),
1334            Path::AngledLineTo { base, .. } => base.tag.clone(),
1335            Path::Base { base } => base.tag.clone(),
1336            Path::TangentialArcTo { base, .. } => base.tag.clone(),
1337            Path::TangentialArc { base, .. } => base.tag.clone(),
1338            Path::Circle { base, .. } => base.tag.clone(),
1339            Path::CircleThreePoint { base, .. } => base.tag.clone(),
1340            Path::Arc { base, .. } => base.tag.clone(),
1341            Path::ArcThreePoint { base, .. } => base.tag.clone(),
1342            Path::Ellipse { base, .. } => base.tag.clone(),
1343            Path::Conic { base, .. } => base.tag.clone(),
1344        }
1345    }
1346
1347    pub fn get_base(&self) -> &BasePath {
1348        match self {
1349            Path::ToPoint { base } => base,
1350            Path::Horizontal { base, .. } => base,
1351            Path::AngledLineTo { base, .. } => base,
1352            Path::Base { base } => base,
1353            Path::TangentialArcTo { base, .. } => base,
1354            Path::TangentialArc { base, .. } => base,
1355            Path::Circle { base, .. } => base,
1356            Path::CircleThreePoint { base, .. } => base,
1357            Path::Arc { base, .. } => base,
1358            Path::ArcThreePoint { base, .. } => base,
1359            Path::Ellipse { base, .. } => base,
1360            Path::Conic { base, .. } => base,
1361        }
1362    }
1363
1364    /// Where does this path segment start?
1365    pub fn get_from(&self) -> [TyF64; 2] {
1366        let p = &self.get_base().from;
1367        let ty: NumericType = self.get_base().units.into();
1368        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1369    }
1370
1371    /// Where does this path segment end?
1372    pub fn get_to(&self) -> [TyF64; 2] {
1373        let p = &self.get_base().to;
1374        let ty: NumericType = self.get_base().units.into();
1375        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1376    }
1377
1378    /// The path segment start point and its type.
1379    pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1380        let p = &self.get_base().from;
1381        let ty: NumericType = self.get_base().units.into();
1382        (*p, ty)
1383    }
1384
1385    /// The path segment end point and its type.
1386    pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1387        let p = &self.get_base().to;
1388        let ty: NumericType = self.get_base().units.into();
1389        (*p, ty)
1390    }
1391
1392    /// Length of this path segment, in cartesian plane. Not all segment types
1393    /// are supported.
1394    pub fn length(&self) -> Option<TyF64> {
1395        let n = match self {
1396            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1397                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1398            }
1399            Self::TangentialArc {
1400                base: _,
1401                center,
1402                ccw: _,
1403            }
1404            | Self::TangentialArcTo {
1405                base: _,
1406                center,
1407                ccw: _,
1408            } => {
1409                // The radius can be calculated as the linear distance between `to` and `center`,
1410                // or between `from` and `center`. They should be the same.
1411                let radius = linear_distance(&self.get_base().from, center);
1412                debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1413                // TODO: Call engine utils to figure this out.
1414                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1415            }
1416            Self::Circle { radius, .. } => Some(2.0 * std::f64::consts::PI * radius),
1417            Self::CircleThreePoint { .. } => {
1418                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1419                    self.get_base().from,
1420                    self.get_base().to,
1421                    self.get_base().to,
1422                ]);
1423                let radius = linear_distance(
1424                    &[circle_center.center[0], circle_center.center[1]],
1425                    &self.get_base().from,
1426                );
1427                Some(2.0 * std::f64::consts::PI * radius)
1428            }
1429            Self::Arc { .. } => {
1430                // TODO: Call engine utils to figure this out.
1431                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1432            }
1433            Self::ArcThreePoint { .. } => {
1434                // TODO: Call engine utils to figure this out.
1435                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1436            }
1437            Self::Ellipse { .. } => {
1438                // Not supported.
1439                None
1440            }
1441            Self::Conic { .. } => {
1442                // Not supported.
1443                None
1444            }
1445        };
1446        n.map(|n| TyF64::new(n, self.get_base().units.into()))
1447    }
1448
1449    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1450        match self {
1451            Path::ToPoint { base } => Some(base),
1452            Path::Horizontal { base, .. } => Some(base),
1453            Path::AngledLineTo { base, .. } => Some(base),
1454            Path::Base { base } => Some(base),
1455            Path::TangentialArcTo { base, .. } => Some(base),
1456            Path::TangentialArc { base, .. } => Some(base),
1457            Path::Circle { base, .. } => Some(base),
1458            Path::CircleThreePoint { base, .. } => Some(base),
1459            Path::Arc { base, .. } => Some(base),
1460            Path::ArcThreePoint { base, .. } => Some(base),
1461            Path::Ellipse { base, .. } => Some(base),
1462            Path::Conic { base, .. } => Some(base),
1463        }
1464    }
1465
1466    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1467        match self {
1468            Path::TangentialArc { center, ccw, .. }
1469            | Path::TangentialArcTo { center, ccw, .. }
1470            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1471                center: *center,
1472                ccw: *ccw,
1473            },
1474            Path::ArcThreePoint { p1, p2, p3, .. } => {
1475                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1476                GetTangentialInfoFromPathsResult::Arc {
1477                    center: circle.center,
1478                    ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1479                }
1480            }
1481            Path::Circle {
1482                center, ccw, radius, ..
1483            } => GetTangentialInfoFromPathsResult::Circle {
1484                center: *center,
1485                ccw: *ccw,
1486                radius: *radius,
1487            },
1488            Path::CircleThreePoint { p1, p2, p3, .. } => {
1489                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1490                let center_point = [circle.center[0], circle.center[1]];
1491                GetTangentialInfoFromPathsResult::Circle {
1492                    center: center_point,
1493                    // Note: a circle is always ccw regardless of the order of points
1494                    ccw: true,
1495                    radius: circle.radius,
1496                }
1497            }
1498            // TODO: (bc) fix me
1499            Path::Ellipse {
1500                center,
1501                major_axis,
1502                minor_radius,
1503                ccw,
1504                ..
1505            } => GetTangentialInfoFromPathsResult::Ellipse {
1506                center: *center,
1507                major_axis: *major_axis,
1508                _minor_radius: *minor_radius,
1509                ccw: *ccw,
1510            },
1511            Path::Conic { .. }
1512            | Path::ToPoint { .. }
1513            | Path::Horizontal { .. }
1514            | Path::AngledLineTo { .. }
1515            | Path::Base { .. } => {
1516                let base = self.get_base();
1517                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1518            }
1519        }
1520    }
1521
1522    /// i.e. not a curve
1523    pub(crate) fn is_straight_line(&self) -> bool {
1524        matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1525    }
1526}
1527
1528/// Compute the straight-line distance between a pair of (2D) points.
1529#[rustfmt::skip]
1530fn linear_distance(
1531    [x0, y0]: &[f64; 2],
1532    [x1, y1]: &[f64; 2]
1533) -> f64 {
1534    let y_sq = (y1 - y0).powi(2);
1535    let x_sq = (x1 - x0).powi(2);
1536    (y_sq + x_sq).sqrt()
1537}
1538
1539/// An extrude surface.
1540#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1541#[ts(export)]
1542#[serde(tag = "type", rename_all = "camelCase")]
1543pub enum ExtrudeSurface {
1544    /// An extrude plane.
1545    ExtrudePlane(ExtrudePlane),
1546    ExtrudeArc(ExtrudeArc),
1547    Chamfer(ChamferSurface),
1548    Fillet(FilletSurface),
1549}
1550
1551// Chamfer surface.
1552#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1553#[ts(export)]
1554#[serde(rename_all = "camelCase")]
1555pub struct ChamferSurface {
1556    /// The id for the chamfer 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// Fillet surface.
1566#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1567#[ts(export)]
1568#[serde(rename_all = "camelCase")]
1569pub struct FilletSurface {
1570    /// The id for the fillet surface.
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 plane.
1580#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1581#[ts(export)]
1582#[serde(rename_all = "camelCase")]
1583pub struct ExtrudePlane {
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
1593/// An extruded arc.
1594#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1595#[ts(export)]
1596#[serde(rename_all = "camelCase")]
1597pub struct ExtrudeArc {
1598    /// The face id for the extrude plane.
1599    pub face_id: uuid::Uuid,
1600    /// The tag.
1601    pub tag: Option<Node<TagDeclarator>>,
1602    /// Metadata.
1603    #[serde(flatten)]
1604    pub geo_meta: GeoMeta,
1605}
1606
1607impl ExtrudeSurface {
1608    pub fn get_id(&self) -> uuid::Uuid {
1609        match self {
1610            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1611            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1612            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1613            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1614        }
1615    }
1616
1617    pub fn face_id(&self) -> uuid::Uuid {
1618        match self {
1619            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id,
1620            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id,
1621            ExtrudeSurface::Fillet(f) => f.face_id,
1622            ExtrudeSurface::Chamfer(c) => c.face_id,
1623        }
1624    }
1625
1626    pub fn set_face_id(&mut self, face_id: uuid::Uuid) {
1627        match self {
1628            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id = face_id,
1629            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id = face_id,
1630            ExtrudeSurface::Fillet(f) => f.face_id = face_id,
1631            ExtrudeSurface::Chamfer(c) => c.face_id = face_id,
1632        }
1633    }
1634
1635    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1636        match self {
1637            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1638            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1639            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1640            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1641        }
1642    }
1643}