kcl_lib/execution/
geometry.rs

1use std::ops::{Add, AddAssign, Mul};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds as kcmc;
6use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq, ModelingCmd};
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    errors::KclError,
13    execution::{ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
14    parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
15    std::sketch::PlaneData,
16};
17
18type Point2D = kcmc::shared::Point2d<f64>;
19type Point3D = kcmc::shared::Point3d<f64>;
20
21/// A geometry.
22#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
23#[ts(export)]
24#[serde(tag = "type")]
25pub enum Geometry {
26    Sketch(Sketch),
27    Solid(Solid),
28}
29
30impl Geometry {
31    pub fn id(&self) -> uuid::Uuid {
32        match self {
33            Geometry::Sketch(s) => s.id,
34            Geometry::Solid(e) => e.id,
35        }
36    }
37
38    /// If this geometry is the result of a pattern, then return the ID of
39    /// the original sketch which was patterned.
40    /// Equivalent to the `id()` method if this isn't a pattern.
41    pub fn original_id(&self) -> uuid::Uuid {
42        match self {
43            Geometry::Sketch(s) => s.original_id,
44            Geometry::Solid(e) => e.sketch.original_id,
45        }
46    }
47}
48
49/// A set of geometry.
50#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
51#[ts(export)]
52#[serde(tag = "type")]
53#[allow(clippy::vec_box)]
54pub enum Geometries {
55    Sketches(Vec<Sketch>),
56    Solids(Vec<Solid>),
57}
58
59impl From<Geometry> for Geometries {
60    fn from(value: Geometry) -> Self {
61        match value {
62            Geometry::Sketch(x) => Self::Sketches(vec![x]),
63            Geometry::Solid(x) => Self::Solids(vec![x]),
64        }
65    }
66}
67
68/// Data for an imported geometry.
69#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
70#[ts(export)]
71#[serde(rename_all = "camelCase")]
72pub struct ImportedGeometry {
73    /// The ID of the imported geometry.
74    pub id: uuid::Uuid,
75    /// The original file paths.
76    pub value: Vec<String>,
77    #[serde(skip)]
78    pub meta: Vec<Metadata>,
79}
80
81/// Data for a solid or an imported geometry.
82#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
83#[ts(export)]
84#[serde(tag = "type", rename_all = "camelCase")]
85#[allow(clippy::vec_box)]
86pub enum SolidOrSketchOrImportedGeometry {
87    ImportedGeometry(Box<ImportedGeometry>),
88    SolidSet(Vec<Solid>),
89    SketchSet(Vec<Sketch>),
90}
91
92impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
93    fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
94        match value {
95            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
96            SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
97                if s.len() == 1 {
98                    crate::execution::KclValue::Solid {
99                        value: Box::new(s.pop().unwrap()),
100                    }
101                } else {
102                    crate::execution::KclValue::HomArray {
103                        value: s
104                            .into_iter()
105                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
106                            .collect(),
107                        ty: crate::execution::types::RuntimeType::solid(),
108                    }
109                }
110            }
111            SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
112                if s.len() == 1 {
113                    crate::execution::KclValue::Sketch {
114                        value: Box::new(s.pop().unwrap()),
115                    }
116                } else {
117                    crate::execution::KclValue::HomArray {
118                        value: s
119                            .into_iter()
120                            .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
121                            .collect(),
122                        ty: crate::execution::types::RuntimeType::sketch(),
123                    }
124                }
125            }
126        }
127    }
128}
129
130impl SolidOrSketchOrImportedGeometry {
131    pub(crate) fn ids(&self) -> Vec<uuid::Uuid> {
132        match self {
133            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => vec![s.id],
134            SolidOrSketchOrImportedGeometry::SolidSet(s) => s.iter().map(|s| s.id).collect(),
135            SolidOrSketchOrImportedGeometry::SketchSet(s) => s.iter().map(|s| s.id).collect(),
136        }
137    }
138}
139
140/// A helix.
141#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
142#[ts(export)]
143#[serde(rename_all = "camelCase")]
144pub struct Helix {
145    /// The id of the helix.
146    pub value: uuid::Uuid,
147    /// The artifact ID.
148    pub artifact_id: ArtifactId,
149    /// Number of revolutions.
150    pub revolutions: f64,
151    /// Start angle (in degrees).
152    pub angle_start: f64,
153    /// Is the helix rotation counter clockwise?
154    pub ccw: bool,
155    /// The cylinder the helix was created on.
156    pub cylinder_id: Option<uuid::Uuid>,
157    pub units: UnitLen,
158    #[serde(skip)]
159    pub meta: Vec<Metadata>,
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
163#[ts(export)]
164#[serde(rename_all = "camelCase")]
165pub struct Plane {
166    /// The id of the plane.
167    pub id: uuid::Uuid,
168    /// The artifact ID.
169    pub artifact_id: ArtifactId,
170    // The code for the plane either a string or custom.
171    pub value: PlaneType,
172    /// Origin of the plane.
173    pub origin: Point3d,
174    /// What should the plane's X axis be?
175    pub x_axis: Point3d,
176    /// What should the plane's Y axis be?
177    pub y_axis: Point3d,
178    /// The z-axis (normal).
179    pub z_axis: Point3d,
180    pub units: UnitLen,
181    #[serde(skip)]
182    pub meta: Vec<Metadata>,
183}
184
185impl Plane {
186    pub(crate) fn into_plane_data(self) -> PlaneData {
187        if self.origin == Point3d::new(0.0, 0.0, 0.0) {
188            match self {
189                Self {
190                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
191                    x_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
192                    y_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
193                    z_axis: Point3d { x: 0.0, y: 0.0, z: 1.0 },
194                    ..
195                } => return PlaneData::XY,
196                Self {
197                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
198                    x_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
199                    y_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
200                    z_axis:
201                        Point3d {
202                            x: 0.0,
203                            y: 0.0,
204                            z: -1.0,
205                        },
206                    ..
207                } => return PlaneData::NegXY,
208                Self {
209                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
210                    x_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
211                    y_axis: Point3d { x: 0.0, y: 0.0, z: 1.0 },
212                    z_axis:
213                        Point3d {
214                            x: 0.0,
215                            y: -1.0,
216                            z: 0.0,
217                        },
218                    ..
219                } => return PlaneData::XZ,
220                Self {
221                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
222                    x_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
223                    y_axis: Point3d { x: 0.0, y: 0.0, z: 1.0 },
224                    z_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
225                    ..
226                } => return PlaneData::NegXZ,
227                Self {
228                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
229                    x_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
230                    y_axis: Point3d { x: 0.0, y: 0.0, z: 1.0 },
231                    z_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
232                    ..
233                } => return PlaneData::YZ,
234                Self {
235                    origin: Point3d { x: 0.0, y: 0.0, z: 0.0 },
236                    x_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
237                    y_axis: Point3d { x: 0.0, y: 0.0, z: 1.0 },
238                    z_axis:
239                        Point3d {
240                            x: -1.0,
241                            y: 0.0,
242                            z: 0.0,
243                        },
244                    ..
245                } => return PlaneData::NegYZ,
246                _ => {}
247            }
248        }
249
250        PlaneData::Plane {
251            origin: self.origin,
252            x_axis: self.x_axis,
253            y_axis: self.y_axis,
254            z_axis: self.z_axis,
255        }
256    }
257
258    pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Self {
259        let id = exec_state.next_uuid();
260        match value {
261            PlaneData::XY => Plane {
262                id,
263                artifact_id: id.into(),
264                origin: Point3d::new(0.0, 0.0, 0.0),
265                x_axis: Point3d::new(1.0, 0.0, 0.0),
266                y_axis: Point3d::new(0.0, 1.0, 0.0),
267                z_axis: Point3d::new(0.0, 0.0, 1.0),
268                value: PlaneType::XY,
269                units: exec_state.length_unit(),
270                meta: vec![],
271            },
272            PlaneData::NegXY => Plane {
273                id,
274                artifact_id: id.into(),
275                origin: Point3d::new(0.0, 0.0, 0.0),
276                x_axis: Point3d::new(1.0, 0.0, 0.0),
277                y_axis: Point3d::new(0.0, 1.0, 0.0),
278                z_axis: Point3d::new(0.0, 0.0, -1.0),
279                value: PlaneType::XY,
280                units: exec_state.length_unit(),
281                meta: vec![],
282            },
283            PlaneData::XZ => Plane {
284                id,
285                artifact_id: id.into(),
286                origin: Point3d::new(0.0, 0.0, 0.0),
287                x_axis: Point3d::new(1.0, 0.0, 0.0),
288                y_axis: Point3d::new(0.0, 0.0, 1.0),
289                z_axis: Point3d::new(0.0, -1.0, 0.0),
290                value: PlaneType::XZ,
291                units: exec_state.length_unit(),
292                meta: vec![],
293            },
294            PlaneData::NegXZ => Plane {
295                id,
296                artifact_id: id.into(),
297                origin: Point3d::new(0.0, 0.0, 0.0),
298                x_axis: Point3d::new(-1.0, 0.0, 0.0),
299                y_axis: Point3d::new(0.0, 0.0, 1.0),
300                z_axis: Point3d::new(0.0, 1.0, 0.0),
301                value: PlaneType::XZ,
302                units: exec_state.length_unit(),
303                meta: vec![],
304            },
305            PlaneData::YZ => Plane {
306                id,
307                artifact_id: id.into(),
308                origin: Point3d::new(0.0, 0.0, 0.0),
309                x_axis: Point3d::new(0.0, 1.0, 0.0),
310                y_axis: Point3d::new(0.0, 0.0, 1.0),
311                z_axis: Point3d::new(1.0, 0.0, 0.0),
312                value: PlaneType::YZ,
313                units: exec_state.length_unit(),
314                meta: vec![],
315            },
316            PlaneData::NegYZ => Plane {
317                id,
318                artifact_id: id.into(),
319                origin: Point3d::new(0.0, 0.0, 0.0),
320                x_axis: Point3d::new(0.0, 1.0, 0.0),
321                y_axis: Point3d::new(0.0, 0.0, 1.0),
322                z_axis: Point3d::new(-1.0, 0.0, 0.0),
323                value: PlaneType::YZ,
324                units: exec_state.length_unit(),
325                meta: vec![],
326            },
327            PlaneData::Plane {
328                origin,
329                x_axis,
330                y_axis,
331                z_axis,
332            } => {
333                let id = exec_state.next_uuid();
334                Plane {
335                    id,
336                    artifact_id: id.into(),
337                    origin,
338                    x_axis,
339                    y_axis,
340                    z_axis,
341                    value: PlaneType::Custom,
342                    units: exec_state.length_unit(),
343                    meta: vec![],
344                }
345            }
346        }
347    }
348
349    /// The standard planes are XY, YZ and XZ (in both positive and negative)
350    pub fn is_standard(&self) -> bool {
351        !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
352    }
353}
354
355/// A face.
356#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
357#[ts(export)]
358#[serde(rename_all = "camelCase")]
359pub struct Face {
360    /// The id of the face.
361    pub id: uuid::Uuid,
362    /// The artifact ID.
363    pub artifact_id: ArtifactId,
364    /// The tag of the face.
365    pub value: String,
366    /// What should the face's X axis be?
367    pub x_axis: Point3d,
368    /// What should the face's Y axis be?
369    pub y_axis: Point3d,
370    /// The z-axis (normal).
371    pub z_axis: Point3d,
372    /// The solid the face is on.
373    pub solid: Box<Solid>,
374    pub units: UnitLen,
375    #[serde(skip)]
376    pub meta: Vec<Metadata>,
377}
378
379/// Type for a plane.
380#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
381#[ts(export)]
382#[display(style = "camelCase")]
383pub enum PlaneType {
384    #[serde(rename = "XY", alias = "xy")]
385    #[display("XY")]
386    XY,
387    #[serde(rename = "XZ", alias = "xz")]
388    #[display("XZ")]
389    XZ,
390    #[serde(rename = "YZ", alias = "yz")]
391    #[display("YZ")]
392    YZ,
393    /// A custom plane.
394    #[display("Custom")]
395    Custom,
396    /// A custom plane which has not been sent to the engine. It must be sent before it is used.
397    #[display("Uninit")]
398    Uninit,
399}
400
401#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
402#[ts(export)]
403#[serde(tag = "type", rename_all = "camelCase")]
404pub struct Sketch {
405    /// The id of the sketch (this will change when the engine's reference to it changes).
406    pub id: uuid::Uuid,
407    /// The paths in the sketch.
408    pub paths: Vec<Path>,
409    /// What the sketch is on (can be a plane or a face).
410    pub on: SketchSurface,
411    /// The starting path.
412    pub start: BasePath,
413    /// Tag identifiers that have been declared in this sketch.
414    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
415    pub tags: IndexMap<String, TagIdentifier>,
416    /// The original id of the sketch. This stays the same even if the sketch is
417    /// is sketched on face etc.
418    pub artifact_id: ArtifactId,
419    #[ts(skip)]
420    pub original_id: uuid::Uuid,
421    pub units: UnitLen,
422    /// Metadata.
423    #[serde(skip)]
424    pub meta: Vec<Metadata>,
425}
426
427impl Sketch {
428    // Tell the engine to enter sketch mode on the sketch.
429    // Run a specific command, then exit sketch mode.
430    pub(crate) fn build_sketch_mode_cmds(
431        &self,
432        exec_state: &mut ExecState,
433        inner_cmd: ModelingCmdReq,
434    ) -> Vec<ModelingCmdReq> {
435        vec![
436            // Before we extrude, we need to enable the sketch mode.
437            // We do this here in case extrude is called out of order.
438            ModelingCmdReq {
439                cmd: ModelingCmd::from(mcmd::EnableSketchMode {
440                    animated: false,
441                    ortho: false,
442                    entity_id: self.on.id(),
443                    adjust_camera: false,
444                    planar_normal: if let SketchSurface::Plane(plane) = &self.on {
445                        // We pass in the normal for the plane here.
446                        Some(plane.z_axis.into())
447                    } else {
448                        None
449                    },
450                }),
451                cmd_id: exec_state.next_uuid().into(),
452            },
453            inner_cmd,
454            ModelingCmdReq {
455                cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
456                cmd_id: exec_state.next_uuid().into(),
457            },
458        ]
459    }
460}
461
462/// A sketch type.
463#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
464#[ts(export)]
465#[serde(tag = "type", rename_all = "camelCase")]
466pub enum SketchSurface {
467    Plane(Box<Plane>),
468    Face(Box<Face>),
469}
470
471impl SketchSurface {
472    pub(crate) fn id(&self) -> uuid::Uuid {
473        match self {
474            SketchSurface::Plane(plane) => plane.id,
475            SketchSurface::Face(face) => face.id,
476        }
477    }
478    pub(crate) fn x_axis(&self) -> Point3d {
479        match self {
480            SketchSurface::Plane(plane) => plane.x_axis,
481            SketchSurface::Face(face) => face.x_axis,
482        }
483    }
484    pub(crate) fn y_axis(&self) -> Point3d {
485        match self {
486            SketchSurface::Plane(plane) => plane.y_axis,
487            SketchSurface::Face(face) => face.y_axis,
488        }
489    }
490    pub(crate) fn z_axis(&self) -> Point3d {
491        match self {
492            SketchSurface::Plane(plane) => plane.z_axis,
493            SketchSurface::Face(face) => face.z_axis,
494        }
495    }
496    pub(crate) fn units(&self) -> UnitLen {
497        match self {
498            SketchSurface::Plane(plane) => plane.units,
499            SketchSurface::Face(face) => face.units,
500        }
501    }
502}
503
504#[derive(Debug, Clone)]
505pub(crate) enum GetTangentialInfoFromPathsResult {
506    PreviousPoint([f64; 2]),
507    Arc { center: [f64; 2], ccw: bool },
508    Circle { center: [f64; 2], ccw: bool, radius: f64 },
509}
510
511impl GetTangentialInfoFromPathsResult {
512    pub(crate) fn tan_previous_point(&self, last_arc_end: crate::std::utils::Coords2d) -> [f64; 2] {
513        match self {
514            GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
515            GetTangentialInfoFromPathsResult::Arc { center, ccw, .. } => {
516                crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
517            }
518            // The circle always starts at 0 degrees, so a suitable tangent
519            // point is either directly above or below.
520            GetTangentialInfoFromPathsResult::Circle {
521                center, radius, ccw, ..
522            } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
523        }
524    }
525}
526
527impl Sketch {
528    pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
529        let mut tag_identifier: TagIdentifier = tag.into();
530        let base = current_path.get_base();
531        tag_identifier.info.push((
532            exec_state.stack().current_epoch(),
533            TagEngineInfo {
534                id: base.geo_meta.id,
535                sketch: self.id,
536                path: Some(current_path.clone()),
537                surface: None,
538            },
539        ));
540
541        self.tags.insert(tag.name.to_string(), tag_identifier);
542    }
543
544    pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
545        for t in tags {
546            match self.tags.get_mut(&t.value) {
547                Some(id) => {
548                    id.merge_info(t);
549                }
550                None => {
551                    self.tags.insert(t.value.clone(), t.clone());
552                }
553            }
554        }
555    }
556
557    /// Get the path most recently sketched.
558    pub(crate) fn latest_path(&self) -> Option<&Path> {
559        self.paths.last()
560    }
561
562    /// The "pen" is an imaginary pen drawing the path.
563    /// This gets the current point the pen is hovering over, i.e. the point
564    /// where the last path segment ends, and the next path segment will begin.
565    pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
566        let Some(path) = self.latest_path() else {
567            return Ok(self.start.to.into());
568        };
569
570        let base = path.get_base();
571        Ok(base.to.into())
572    }
573
574    pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
575        let Some(path) = self.latest_path() else {
576            return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
577        };
578        path.get_tangential_info()
579    }
580}
581
582#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
583#[ts(export)]
584#[serde(tag = "type", rename_all = "camelCase")]
585pub struct Solid {
586    /// The id of the solid.
587    pub id: uuid::Uuid,
588    /// The artifact ID of the solid.  Unlike `id`, this doesn't change.
589    pub artifact_id: ArtifactId,
590    /// The extrude surfaces.
591    pub value: Vec<ExtrudeSurface>,
592    /// The sketch.
593    pub sketch: Sketch,
594    /// The height of the solid.
595    pub height: f64,
596    /// The id of the extrusion start cap
597    pub start_cap_id: Option<uuid::Uuid>,
598    /// The id of the extrusion end cap
599    pub end_cap_id: Option<uuid::Uuid>,
600    /// Chamfers or fillets on this solid.
601    #[serde(default, skip_serializing_if = "Vec::is_empty")]
602    pub edge_cuts: Vec<EdgeCut>,
603    pub units: UnitLen,
604    /// Metadata.
605    #[serde(skip)]
606    pub meta: Vec<Metadata>,
607}
608
609impl Solid {
610    pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
611        self.edge_cuts.iter().map(|foc| foc.id())
612    }
613}
614
615/// A fillet or a chamfer.
616#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
617#[ts(export)]
618#[serde(tag = "type", rename_all = "camelCase")]
619pub enum EdgeCut {
620    /// A fillet.
621    Fillet {
622        /// The id of the engine command that called this fillet.
623        id: uuid::Uuid,
624        radius: f64,
625        /// The engine id of the edge to fillet.
626        #[serde(rename = "edgeId")]
627        edge_id: uuid::Uuid,
628        tag: Box<Option<TagNode>>,
629    },
630    /// A chamfer.
631    Chamfer {
632        /// The id of the engine command that called this chamfer.
633        id: uuid::Uuid,
634        length: f64,
635        /// The engine id of the edge to chamfer.
636        #[serde(rename = "edgeId")]
637        edge_id: uuid::Uuid,
638        tag: Box<Option<TagNode>>,
639    },
640}
641
642impl EdgeCut {
643    pub fn id(&self) -> uuid::Uuid {
644        match self {
645            EdgeCut::Fillet { id, .. } => *id,
646            EdgeCut::Chamfer { id, .. } => *id,
647        }
648    }
649
650    pub fn edge_id(&self) -> uuid::Uuid {
651        match self {
652            EdgeCut::Fillet { edge_id, .. } => *edge_id,
653            EdgeCut::Chamfer { edge_id, .. } => *edge_id,
654        }
655    }
656
657    pub fn tag(&self) -> Option<TagNode> {
658        match self {
659            EdgeCut::Fillet { tag, .. } => *tag.clone(),
660            EdgeCut::Chamfer { tag, .. } => *tag.clone(),
661        }
662    }
663}
664
665#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
666#[ts(export)]
667pub struct Point2d {
668    pub x: f64,
669    pub y: f64,
670}
671
672impl From<[f64; 2]> for Point2d {
673    fn from(p: [f64; 2]) -> Self {
674        Self { x: p[0], y: p[1] }
675    }
676}
677
678impl From<&[f64; 2]> for Point2d {
679    fn from(p: &[f64; 2]) -> Self {
680        Self { x: p[0], y: p[1] }
681    }
682}
683
684impl From<Point2d> for [f64; 2] {
685    fn from(p: Point2d) -> Self {
686        [p.x, p.y]
687    }
688}
689
690impl From<Point2d> for Point2D {
691    fn from(p: Point2d) -> Self {
692        Self { x: p.x, y: p.y }
693    }
694}
695
696impl Point2d {
697    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
698    pub fn scale(self, scalar: f64) -> Self {
699        Self {
700            x: self.x * scalar,
701            y: self.y * scalar,
702        }
703    }
704}
705
706#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
707#[ts(export)]
708pub struct Point3d {
709    pub x: f64,
710    pub y: f64,
711    pub z: f64,
712}
713
714impl Point3d {
715    pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 };
716    pub fn new(x: f64, y: f64, z: f64) -> Self {
717        Self { x, y, z }
718    }
719}
720
721impl From<Point3d> for Point3D {
722    fn from(p: Point3d) -> Self {
723        Self { x: p.x, y: p.y, z: p.z }
724    }
725}
726impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
727    fn from(p: Point3d) -> Self {
728        Self {
729            x: LengthUnit(p.x),
730            y: LengthUnit(p.y),
731            z: LengthUnit(p.z),
732        }
733    }
734}
735
736impl Add for Point3d {
737    type Output = Point3d;
738
739    fn add(self, rhs: Self) -> Self::Output {
740        Point3d {
741            x: self.x + rhs.x,
742            y: self.y + rhs.y,
743            z: self.z + rhs.z,
744        }
745    }
746}
747
748impl AddAssign for Point3d {
749    fn add_assign(&mut self, rhs: Self) {
750        *self = *self + rhs
751    }
752}
753
754impl Mul<f64> for Point3d {
755    type Output = Point3d;
756
757    fn mul(self, rhs: f64) -> Self::Output {
758        Point3d {
759            x: self.x * rhs,
760            y: self.y * rhs,
761            z: self.z * rhs,
762        }
763    }
764}
765
766/// A base path.
767#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
768#[ts(export)]
769#[serde(rename_all = "camelCase")]
770pub struct BasePath {
771    /// The from point.
772    #[ts(type = "[number, number]")]
773    pub from: [f64; 2],
774    /// The to point.
775    #[ts(type = "[number, number]")]
776    pub to: [f64; 2],
777    pub units: UnitLen,
778    /// The tag of the path.
779    pub tag: Option<TagNode>,
780    /// Metadata.
781    #[serde(rename = "__geoMeta")]
782    pub geo_meta: GeoMeta,
783}
784
785/// Geometry metadata.
786#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
787#[ts(export)]
788#[serde(rename_all = "camelCase")]
789pub struct GeoMeta {
790    /// The id of the geometry.
791    pub id: uuid::Uuid,
792    /// Metadata.
793    #[serde(flatten)]
794    pub metadata: Metadata,
795}
796
797/// A path.
798#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
799#[ts(export)]
800#[serde(tag = "type")]
801pub enum Path {
802    /// A path that goes to a point.
803    ToPoint {
804        #[serde(flatten)]
805        base: BasePath,
806    },
807    /// A arc that is tangential to the last path segment that goes to a point
808    TangentialArcTo {
809        #[serde(flatten)]
810        base: BasePath,
811        /// the arc's center
812        #[ts(type = "[number, number]")]
813        center: [f64; 2],
814        /// arc's direction
815        ccw: bool,
816    },
817    /// A arc that is tangential to the last path segment
818    TangentialArc {
819        #[serde(flatten)]
820        base: BasePath,
821        /// the arc's center
822        #[ts(type = "[number, number]")]
823        center: [f64; 2],
824        /// arc's direction
825        ccw: bool,
826    },
827    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
828    /// a complete arc
829    Circle {
830        #[serde(flatten)]
831        base: BasePath,
832        /// the arc's center
833        #[ts(type = "[number, number]")]
834        center: [f64; 2],
835        /// the arc's radius
836        radius: f64,
837        /// arc's direction
838        /// This is used to compute the tangential angle.
839        ccw: bool,
840    },
841    CircleThreePoint {
842        #[serde(flatten)]
843        base: BasePath,
844        /// Point 1 of the circle
845        #[ts(type = "[number, number]")]
846        p1: [f64; 2],
847        /// Point 2 of the circle
848        #[ts(type = "[number, number]")]
849        p2: [f64; 2],
850        /// Point 3 of the circle
851        #[ts(type = "[number, number]")]
852        p3: [f64; 2],
853    },
854    ArcThreePoint {
855        #[serde(flatten)]
856        base: BasePath,
857        /// Point 1 of the arc (base on the end of previous segment)
858        #[ts(type = "[number, number]")]
859        p1: [f64; 2],
860        /// Point 2 of the arc (interior kwarg)
861        #[ts(type = "[number, number]")]
862        p2: [f64; 2],
863        /// Point 3 of the arc (end kwarg)
864        #[ts(type = "[number, number]")]
865        p3: [f64; 2],
866    },
867    /// A path that is horizontal.
868    Horizontal {
869        #[serde(flatten)]
870        base: BasePath,
871        /// The x coordinate.
872        x: f64,
873    },
874    /// An angled line to.
875    AngledLineTo {
876        #[serde(flatten)]
877        base: BasePath,
878        /// The x coordinate.
879        x: Option<f64>,
880        /// The y coordinate.
881        y: Option<f64>,
882    },
883    /// A base path.
884    Base {
885        #[serde(flatten)]
886        base: BasePath,
887    },
888    /// A circular arc, not necessarily tangential to the current point.
889    Arc {
890        #[serde(flatten)]
891        base: BasePath,
892        /// Center of the circle that this arc is drawn on.
893        center: [f64; 2],
894        /// Radius of the circle that this arc is drawn on.
895        radius: f64,
896        /// True if the arc is counterclockwise.
897        ccw: bool,
898    },
899}
900
901/// What kind of path is this?
902#[derive(Display)]
903enum PathType {
904    ToPoint,
905    Base,
906    TangentialArc,
907    TangentialArcTo,
908    Circle,
909    CircleThreePoint,
910    Horizontal,
911    AngledLineTo,
912    Arc,
913}
914
915impl From<&Path> for PathType {
916    fn from(value: &Path) -> Self {
917        match value {
918            Path::ToPoint { .. } => Self::ToPoint,
919            Path::TangentialArcTo { .. } => Self::TangentialArcTo,
920            Path::TangentialArc { .. } => Self::TangentialArc,
921            Path::Circle { .. } => Self::Circle,
922            Path::CircleThreePoint { .. } => Self::CircleThreePoint,
923            Path::Horizontal { .. } => Self::Horizontal,
924            Path::AngledLineTo { .. } => Self::AngledLineTo,
925            Path::Base { .. } => Self::Base,
926            Path::Arc { .. } => Self::Arc,
927            Path::ArcThreePoint { .. } => Self::Arc,
928        }
929    }
930}
931
932impl Path {
933    pub fn get_id(&self) -> uuid::Uuid {
934        match self {
935            Path::ToPoint { base } => base.geo_meta.id,
936            Path::Horizontal { base, .. } => base.geo_meta.id,
937            Path::AngledLineTo { base, .. } => base.geo_meta.id,
938            Path::Base { base } => base.geo_meta.id,
939            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
940            Path::TangentialArc { base, .. } => base.geo_meta.id,
941            Path::Circle { base, .. } => base.geo_meta.id,
942            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
943            Path::Arc { base, .. } => base.geo_meta.id,
944            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
945        }
946    }
947
948    pub fn get_tag(&self) -> Option<TagNode> {
949        match self {
950            Path::ToPoint { base } => base.tag.clone(),
951            Path::Horizontal { base, .. } => base.tag.clone(),
952            Path::AngledLineTo { base, .. } => base.tag.clone(),
953            Path::Base { base } => base.tag.clone(),
954            Path::TangentialArcTo { base, .. } => base.tag.clone(),
955            Path::TangentialArc { base, .. } => base.tag.clone(),
956            Path::Circle { base, .. } => base.tag.clone(),
957            Path::CircleThreePoint { base, .. } => base.tag.clone(),
958            Path::Arc { base, .. } => base.tag.clone(),
959            Path::ArcThreePoint { base, .. } => base.tag.clone(),
960        }
961    }
962
963    pub fn get_base(&self) -> &BasePath {
964        match self {
965            Path::ToPoint { base } => base,
966            Path::Horizontal { base, .. } => base,
967            Path::AngledLineTo { base, .. } => base,
968            Path::Base { base } => base,
969            Path::TangentialArcTo { base, .. } => base,
970            Path::TangentialArc { base, .. } => base,
971            Path::Circle { base, .. } => base,
972            Path::CircleThreePoint { base, .. } => base,
973            Path::Arc { base, .. } => base,
974            Path::ArcThreePoint { base, .. } => base,
975        }
976    }
977
978    /// Where does this path segment start?
979    pub fn get_from(&self) -> &[f64; 2] {
980        &self.get_base().from
981    }
982    /// Where does this path segment end?
983    pub fn get_to(&self) -> &[f64; 2] {
984        &self.get_base().to
985    }
986
987    /// Length of this path segment, in cartesian plane.
988    pub fn length(&self) -> f64 {
989        match self {
990            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
991                linear_distance(self.get_from(), self.get_to())
992            }
993            Self::TangentialArc {
994                base: _,
995                center,
996                ccw: _,
997            }
998            | Self::TangentialArcTo {
999                base: _,
1000                center,
1001                ccw: _,
1002            } => {
1003                // The radius can be calculated as the linear distance between `to` and `center`,
1004                // or between `from` and `center`. They should be the same.
1005                let radius = linear_distance(self.get_from(), center);
1006                debug_assert_eq!(radius, linear_distance(self.get_to(), center));
1007                // TODO: Call engine utils to figure this out.
1008                linear_distance(self.get_from(), self.get_to())
1009            }
1010            Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1011            Self::CircleThreePoint { .. } => {
1012                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1013                    self.get_base().from.into(),
1014                    self.get_base().to.into(),
1015                    self.get_base().to.into(),
1016                ]);
1017                let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &self.get_base().from);
1018                2.0 * std::f64::consts::PI * radius
1019            }
1020            Self::Arc { .. } => {
1021                // TODO: Call engine utils to figure this out.
1022                linear_distance(self.get_from(), self.get_to())
1023            }
1024            Self::ArcThreePoint { .. } => {
1025                // TODO: Call engine utils to figure this out.
1026                linear_distance(self.get_from(), self.get_to())
1027            }
1028        }
1029    }
1030
1031    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1032        match self {
1033            Path::ToPoint { base } => Some(base),
1034            Path::Horizontal { base, .. } => Some(base),
1035            Path::AngledLineTo { base, .. } => Some(base),
1036            Path::Base { base } => Some(base),
1037            Path::TangentialArcTo { base, .. } => Some(base),
1038            Path::TangentialArc { base, .. } => Some(base),
1039            Path::Circle { base, .. } => Some(base),
1040            Path::CircleThreePoint { base, .. } => Some(base),
1041            Path::Arc { base, .. } => Some(base),
1042            Path::ArcThreePoint { base, .. } => Some(base),
1043        }
1044    }
1045
1046    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1047        match self {
1048            Path::TangentialArc { center, ccw, .. }
1049            | Path::TangentialArcTo { center, ccw, .. }
1050            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1051                center: *center,
1052                ccw: *ccw,
1053            },
1054            Path::ArcThreePoint { p1, p2, p3, .. } => {
1055                let circle_center =
1056                    crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
1057                let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
1058                let center_point = [circle_center.center.x, circle_center.center.y];
1059                GetTangentialInfoFromPathsResult::Circle {
1060                    center: center_point,
1061                    ccw: true,
1062                    radius,
1063                }
1064            }
1065            Path::Circle {
1066                center, ccw, radius, ..
1067            } => GetTangentialInfoFromPathsResult::Circle {
1068                center: *center,
1069                ccw: *ccw,
1070                radius: *radius,
1071            },
1072            Path::CircleThreePoint { p1, p2, p3, .. } => {
1073                let circle_center =
1074                    crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
1075                let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
1076                let center_point = [circle_center.center.x, circle_center.center.y];
1077                GetTangentialInfoFromPathsResult::Circle {
1078                    center: center_point,
1079                    ccw: true,
1080                    radius,
1081                }
1082            }
1083            Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1084                let base = self.get_base();
1085                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1086            }
1087        }
1088    }
1089}
1090
1091/// Compute the straight-line distance between a pair of (2D) points.
1092#[rustfmt::skip]
1093fn linear_distance(
1094    [x0, y0]: &[f64; 2],
1095    [x1, y1]: &[f64; 2]
1096) -> f64 {
1097    let y_sq = (y1 - y0).powi(2);
1098    let x_sq = (x1 - x0).powi(2);
1099    (y_sq + x_sq).sqrt()
1100}
1101
1102/// An extrude surface.
1103#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1104#[ts(export)]
1105#[serde(tag = "type", rename_all = "camelCase")]
1106pub enum ExtrudeSurface {
1107    /// An extrude plane.
1108    ExtrudePlane(ExtrudePlane),
1109    ExtrudeArc(ExtrudeArc),
1110    Chamfer(ChamferSurface),
1111    Fillet(FilletSurface),
1112}
1113
1114// Chamfer surface.
1115#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1116#[ts(export)]
1117#[serde(rename_all = "camelCase")]
1118pub struct ChamferSurface {
1119    /// The id for the chamfer surface.
1120    pub face_id: uuid::Uuid,
1121    /// The tag.
1122    pub tag: Option<Node<TagDeclarator>>,
1123    /// Metadata.
1124    #[serde(flatten)]
1125    pub geo_meta: GeoMeta,
1126}
1127
1128// Fillet surface.
1129#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1130#[ts(export)]
1131#[serde(rename_all = "camelCase")]
1132pub struct FilletSurface {
1133    /// The id for the fillet surface.
1134    pub face_id: uuid::Uuid,
1135    /// The tag.
1136    pub tag: Option<Node<TagDeclarator>>,
1137    /// Metadata.
1138    #[serde(flatten)]
1139    pub geo_meta: GeoMeta,
1140}
1141
1142/// An extruded plane.
1143#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1144#[ts(export)]
1145#[serde(rename_all = "camelCase")]
1146pub struct ExtrudePlane {
1147    /// The face id for the extrude plane.
1148    pub face_id: uuid::Uuid,
1149    /// The tag.
1150    pub tag: Option<Node<TagDeclarator>>,
1151    /// Metadata.
1152    #[serde(flatten)]
1153    pub geo_meta: GeoMeta,
1154}
1155
1156/// An extruded arc.
1157#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1158#[ts(export)]
1159#[serde(rename_all = "camelCase")]
1160pub struct ExtrudeArc {
1161    /// The face id for the extrude plane.
1162    pub face_id: uuid::Uuid,
1163    /// The tag.
1164    pub tag: Option<Node<TagDeclarator>>,
1165    /// Metadata.
1166    #[serde(flatten)]
1167    pub geo_meta: GeoMeta,
1168}
1169
1170impl ExtrudeSurface {
1171    pub fn get_id(&self) -> uuid::Uuid {
1172        match self {
1173            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1174            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1175            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1176            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1177        }
1178    }
1179
1180    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1181        match self {
1182            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1183            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1184            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1185            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1186        }
1187    }
1188}