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