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