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