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