kcl_lib/execution/
geometry.rs

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