kcl_lib/execution/
geometry.rs

1use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_error::SourceRange;
6use kittycad_modeling_cmds as kcmc;
7use kittycad_modeling_cmds::{
8    ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, units::UnitLength, websocket::ModelingCmdReq,
9};
10use parse_display::{Display, FromStr};
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    engine::{DEFAULT_PLANE_INFO, PlaneName},
15    errors::{KclError, KclErrorDetails},
16    exec::KclValue,
17    execution::{
18        ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, normalize_to_solver_unit,
19        types::{NumericType, adjust_length},
20    },
21    front::{ArcCtor, Freedom, LineCtor, ObjectId, PointCtor},
22    parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
23    std::{args::TyF64, sketch::PlaneData},
24};
25
26type Point3D = kcmc::shared::Point3d<f64>;
27
28/// A GD&T annotation.
29#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
30#[ts(export)]
31#[serde(tag = "type", rename_all = "camelCase")]
32pub struct GdtAnnotation {
33    /// The engine ID.
34    pub id: uuid::Uuid,
35    #[serde(skip)]
36    pub meta: Vec<Metadata>,
37}
38
39/// A geometry.
40#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
41#[ts(export)]
42#[serde(tag = "type")]
43#[allow(clippy::large_enum_variant)]
44pub enum Geometry {
45    Sketch(Sketch),
46    Solid(Solid),
47}
48
49impl Geometry {
50    pub fn id(&self) -> uuid::Uuid {
51        match self {
52            Geometry::Sketch(s) => s.id,
53            Geometry::Solid(e) => e.id,
54        }
55    }
56
57    /// If this geometry is the result of a pattern, then return the ID of
58    /// the original sketch which was patterned.
59    /// Equivalent to the `id()` method if this isn't a pattern.
60    pub fn original_id(&self) -> uuid::Uuid {
61        match self {
62            Geometry::Sketch(s) => s.original_id,
63            Geometry::Solid(e) => e.sketch.original_id,
64        }
65    }
66}
67
68/// A geometry including an imported geometry.
69#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
70#[ts(export)]
71#[serde(tag = "type")]
72#[allow(clippy::large_enum_variant)]
73pub enum GeometryWithImportedGeometry {
74    Sketch(Sketch),
75    Solid(Solid),
76    ImportedGeometry(Box<ImportedGeometry>),
77}
78
79impl GeometryWithImportedGeometry {
80    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
81        match self {
82            GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
83            GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
84            GeometryWithImportedGeometry::ImportedGeometry(i) => {
85                let id = i.id(ctx).await?;
86                Ok(id)
87            }
88        }
89    }
90}
91
92/// A set of geometry.
93#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
94#[ts(export)]
95#[serde(tag = "type")]
96#[allow(clippy::vec_box)]
97pub enum Geometries {
98    Sketches(Vec<Sketch>),
99    Solids(Vec<Solid>),
100}
101
102impl From<Geometry> for Geometries {
103    fn from(value: Geometry) -> Self {
104        match value {
105            Geometry::Sketch(x) => Self::Sketches(vec![x]),
106            Geometry::Solid(x) => Self::Solids(vec![x]),
107        }
108    }
109}
110
111/// Data for an imported geometry.
112#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
113#[ts(export)]
114#[serde(rename_all = "camelCase")]
115pub struct ImportedGeometry {
116    /// The ID of the imported geometry.
117    pub id: uuid::Uuid,
118    /// The original file paths.
119    pub value: Vec<String>,
120    #[serde(skip)]
121    pub meta: Vec<Metadata>,
122    /// If the imported geometry has completed.
123    #[serde(skip)]
124    completed: bool,
125}
126
127impl ImportedGeometry {
128    pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
129        Self {
130            id,
131            value,
132            meta,
133            completed: false,
134        }
135    }
136
137    async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
138        if self.completed {
139            return Ok(());
140        }
141
142        ctx.engine
143            .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
144            .await?;
145
146        self.completed = true;
147
148        Ok(())
149    }
150
151    pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
152        if !self.completed {
153            self.wait_for_finish(ctx).await?;
154        }
155
156        Ok(self.id)
157    }
158}
159
160/// Data for a solid, sketch, or an imported geometry.
161#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
162#[ts(export)]
163#[serde(tag = "type", rename_all = "camelCase")]
164#[allow(clippy::vec_box)]
165pub enum SolidOrSketchOrImportedGeometry {
166    ImportedGeometry(Box<ImportedGeometry>),
167    SolidSet(Vec<Solid>),
168    SketchSet(Vec<Sketch>),
169}
170
171impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
172    fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
173        match value {
174            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
175            SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
176                if s.len() == 1
177                    && let Some(s) = s.pop()
178                {
179                    crate::execution::KclValue::Solid { value: Box::new(s) }
180                } else {
181                    crate::execution::KclValue::HomArray {
182                        value: s
183                            .into_iter()
184                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
185                            .collect(),
186                        ty: crate::execution::types::RuntimeType::solid(),
187                    }
188                }
189            }
190            SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
191                if s.len() == 1
192                    && let Some(s) = s.pop()
193                {
194                    crate::execution::KclValue::Sketch { value: Box::new(s) }
195                } else {
196                    crate::execution::KclValue::HomArray {
197                        value: s
198                            .into_iter()
199                            .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
200                            .collect(),
201                        ty: crate::execution::types::RuntimeType::sketch(),
202                    }
203                }
204            }
205        }
206    }
207}
208
209impl SolidOrSketchOrImportedGeometry {
210    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
211        match self {
212            SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
213                let id = s.id(ctx).await?;
214
215                Ok(vec![id])
216            }
217            SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
218            SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
219        }
220    }
221}
222
223/// Data for a solid or an imported geometry.
224#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
225#[ts(export)]
226#[serde(tag = "type", rename_all = "camelCase")]
227#[allow(clippy::vec_box)]
228pub enum SolidOrImportedGeometry {
229    ImportedGeometry(Box<ImportedGeometry>),
230    SolidSet(Vec<Solid>),
231}
232
233impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
234    fn from(value: SolidOrImportedGeometry) -> Self {
235        match value {
236            SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
237            SolidOrImportedGeometry::SolidSet(mut s) => {
238                if s.len() == 1
239                    && let Some(s) = s.pop()
240                {
241                    crate::execution::KclValue::Solid { value: Box::new(s) }
242                } else {
243                    crate::execution::KclValue::HomArray {
244                        value: s
245                            .into_iter()
246                            .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
247                            .collect(),
248                        ty: crate::execution::types::RuntimeType::solid(),
249                    }
250                }
251            }
252        }
253    }
254}
255
256impl SolidOrImportedGeometry {
257    pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
258        match self {
259            SolidOrImportedGeometry::ImportedGeometry(s) => {
260                let id = s.id(ctx).await?;
261
262                Ok(vec![id])
263            }
264            SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
265        }
266    }
267}
268
269/// A helix.
270#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
271#[ts(export)]
272#[serde(rename_all = "camelCase")]
273pub struct Helix {
274    /// The id of the helix.
275    pub value: uuid::Uuid,
276    /// The artifact ID.
277    pub artifact_id: ArtifactId,
278    /// Number of revolutions.
279    pub revolutions: f64,
280    /// Start angle (in degrees).
281    pub angle_start: f64,
282    /// Is the helix rotation counter clockwise?
283    pub ccw: bool,
284    /// The cylinder the helix was created on.
285    pub cylinder_id: Option<uuid::Uuid>,
286    pub units: UnitLength,
287    #[serde(skip)]
288    pub meta: Vec<Metadata>,
289}
290
291#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
292#[ts(export)]
293#[serde(rename_all = "camelCase")]
294pub struct Plane {
295    /// The id of the plane.
296    pub id: uuid::Uuid,
297    /// The artifact ID.
298    pub artifact_id: ArtifactId,
299    /// The scene object ID.
300    // TODO: This shouldn't be an Option. It should be part of the [`PlaneType`]
301    // enum since it's only none when `Uninit`.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub object_id: Option<ObjectId>,
304    // The code for the plane either a string or custom.
305    pub value: PlaneType,
306    /// The information for the plane.
307    #[serde(flatten)]
308    pub info: PlaneInfo,
309    #[serde(skip)]
310    pub meta: Vec<Metadata>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
314#[ts(export)]
315#[serde(rename_all = "camelCase")]
316pub struct PlaneInfo {
317    /// Origin of the plane.
318    pub origin: Point3d,
319    /// What should the plane's X axis be?
320    pub x_axis: Point3d,
321    /// What should the plane's Y axis be?
322    pub y_axis: Point3d,
323    /// What should the plane's Z axis be?
324    pub z_axis: Point3d,
325}
326
327impl PlaneInfo {
328    pub(crate) fn into_plane_data(self) -> PlaneData {
329        if self.origin.is_zero() {
330            match self {
331                Self {
332                    origin:
333                        Point3d {
334                            x: 0.0,
335                            y: 0.0,
336                            z: 0.0,
337                            units: Some(UnitLength::Millimeters),
338                        },
339                    x_axis:
340                        Point3d {
341                            x: 1.0,
342                            y: 0.0,
343                            z: 0.0,
344                            units: _,
345                        },
346                    y_axis:
347                        Point3d {
348                            x: 0.0,
349                            y: 1.0,
350                            z: 0.0,
351                            units: _,
352                        },
353                    z_axis: _,
354                } => return PlaneData::XY,
355                Self {
356                    origin:
357                        Point3d {
358                            x: 0.0,
359                            y: 0.0,
360                            z: 0.0,
361                            units: Some(UnitLength::Millimeters),
362                        },
363                    x_axis:
364                        Point3d {
365                            x: -1.0,
366                            y: 0.0,
367                            z: 0.0,
368                            units: _,
369                        },
370                    y_axis:
371                        Point3d {
372                            x: 0.0,
373                            y: 1.0,
374                            z: 0.0,
375                            units: _,
376                        },
377                    z_axis: _,
378                } => return PlaneData::NegXY,
379                Self {
380                    origin:
381                        Point3d {
382                            x: 0.0,
383                            y: 0.0,
384                            z: 0.0,
385                            units: Some(UnitLength::Millimeters),
386                        },
387                    x_axis:
388                        Point3d {
389                            x: 1.0,
390                            y: 0.0,
391                            z: 0.0,
392                            units: _,
393                        },
394                    y_axis:
395                        Point3d {
396                            x: 0.0,
397                            y: 0.0,
398                            z: 1.0,
399                            units: _,
400                        },
401                    z_axis: _,
402                } => return PlaneData::XZ,
403                Self {
404                    origin:
405                        Point3d {
406                            x: 0.0,
407                            y: 0.0,
408                            z: 0.0,
409                            units: Some(UnitLength::Millimeters),
410                        },
411                    x_axis:
412                        Point3d {
413                            x: -1.0,
414                            y: 0.0,
415                            z: 0.0,
416                            units: _,
417                        },
418                    y_axis:
419                        Point3d {
420                            x: 0.0,
421                            y: 0.0,
422                            z: 1.0,
423                            units: _,
424                        },
425                    z_axis: _,
426                } => return PlaneData::NegXZ,
427                Self {
428                    origin:
429                        Point3d {
430                            x: 0.0,
431                            y: 0.0,
432                            z: 0.0,
433                            units: Some(UnitLength::Millimeters),
434                        },
435                    x_axis:
436                        Point3d {
437                            x: 0.0,
438                            y: 1.0,
439                            z: 0.0,
440                            units: _,
441                        },
442                    y_axis:
443                        Point3d {
444                            x: 0.0,
445                            y: 0.0,
446                            z: 1.0,
447                            units: _,
448                        },
449                    z_axis: _,
450                } => return PlaneData::YZ,
451                Self {
452                    origin:
453                        Point3d {
454                            x: 0.0,
455                            y: 0.0,
456                            z: 0.0,
457                            units: Some(UnitLength::Millimeters),
458                        },
459                    x_axis:
460                        Point3d {
461                            x: 0.0,
462                            y: -1.0,
463                            z: 0.0,
464                            units: _,
465                        },
466                    y_axis:
467                        Point3d {
468                            x: 0.0,
469                            y: 0.0,
470                            z: 1.0,
471                            units: _,
472                        },
473                    z_axis: _,
474                } => return PlaneData::NegYZ,
475                _ => {}
476            }
477        }
478
479        PlaneData::Plane(Self {
480            origin: self.origin,
481            x_axis: self.x_axis,
482            y_axis: self.y_axis,
483            z_axis: self.z_axis,
484        })
485    }
486
487    pub(crate) fn is_right_handed(&self) -> bool {
488        // Katie's formula:
489        // dot(cross(x, y), z) ~= sqrt(dot(x, x) * dot(y, y) * dot(z, z))
490        let lhs = self
491            .x_axis
492            .axes_cross_product(&self.y_axis)
493            .axes_dot_product(&self.z_axis);
494        let rhs_x = self.x_axis.axes_dot_product(&self.x_axis);
495        let rhs_y = self.y_axis.axes_dot_product(&self.y_axis);
496        let rhs_z = self.z_axis.axes_dot_product(&self.z_axis);
497        let rhs = (rhs_x * rhs_y * rhs_z).sqrt();
498        // Check LHS ~= RHS
499        (lhs - rhs).abs() <= 0.0001
500    }
501
502    #[cfg(test)]
503    pub(crate) fn is_left_handed(&self) -> bool {
504        !self.is_right_handed()
505    }
506
507    pub(crate) fn make_right_handed(self) -> Self {
508        if self.is_right_handed() {
509            return self;
510        }
511        // To make it right-handed, negate X, i.e. rotate the plane 180 degrees.
512        Self {
513            origin: self.origin,
514            x_axis: self.x_axis.negated(),
515            y_axis: self.y_axis,
516            z_axis: self.z_axis,
517        }
518    }
519}
520
521impl TryFrom<PlaneData> for PlaneInfo {
522    type Error = KclError;
523
524    fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
525        if let PlaneData::Plane(info) = value {
526            return Ok(info);
527        }
528        let name = match value {
529            PlaneData::XY => PlaneName::Xy,
530            PlaneData::NegXY => PlaneName::NegXy,
531            PlaneData::XZ => PlaneName::Xz,
532            PlaneData::NegXZ => PlaneName::NegXz,
533            PlaneData::YZ => PlaneName::Yz,
534            PlaneData::NegYZ => PlaneName::NegYz,
535            PlaneData::Plane(_) => {
536                // We will never get here since we already checked for PlaneData::Plane.
537                return Err(KclError::new_internal(KclErrorDetails::new(
538                    format!("PlaneData {value:?} not found"),
539                    Default::default(),
540                )));
541            }
542        };
543
544        let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
545            KclError::new_internal(KclErrorDetails::new(
546                format!("Plane {name} not found"),
547                Default::default(),
548            ))
549        })?;
550
551        Ok(info.clone())
552    }
553}
554
555impl From<PlaneData> for PlaneType {
556    fn from(value: PlaneData) -> Self {
557        match value {
558            PlaneData::XY => PlaneType::XY,
559            PlaneData::NegXY => PlaneType::XY,
560            PlaneData::XZ => PlaneType::XZ,
561            PlaneData::NegXZ => PlaneType::XZ,
562            PlaneData::YZ => PlaneType::YZ,
563            PlaneData::NegYZ => PlaneType::YZ,
564            PlaneData::Plane(_) => PlaneType::Custom,
565        }
566    }
567}
568
569impl Plane {
570    pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
571        let id = exec_state.next_uuid();
572        Ok(Plane {
573            id,
574            object_id: None,
575            artifact_id: id.into(),
576            info: PlaneInfo::try_from(value.clone())?,
577            value: value.into(),
578            meta: vec![],
579        })
580    }
581
582    /// The standard planes are XY, YZ and XZ (in both positive and negative)
583    pub fn is_standard(&self) -> bool {
584        !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
585    }
586
587    /// Project a point onto a plane by calculating how far away it is and moving it along the
588    /// normal of the plane so that it now lies on the plane.
589    pub fn project(&self, point: Point3d) -> Point3d {
590        let v = point - self.info.origin;
591        let dot = v.axes_dot_product(&self.info.z_axis);
592
593        point - self.info.z_axis * dot
594    }
595}
596
597/// A face.
598#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
599#[ts(export)]
600#[serde(rename_all = "camelCase")]
601pub struct Face {
602    /// The id of the face.
603    pub id: uuid::Uuid,
604    /// The artifact ID.
605    pub artifact_id: ArtifactId,
606    /// The tag of the face.
607    pub value: String,
608    /// What should the face's X axis be?
609    pub x_axis: Point3d,
610    /// What should the face's Y axis be?
611    pub y_axis: Point3d,
612    /// The solid the face is on.
613    pub solid: Box<Solid>,
614    pub units: UnitLength,
615    #[serde(skip)]
616    pub meta: Vec<Metadata>,
617}
618
619/// Type for a plane.
620#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
621#[ts(export)]
622#[display(style = "camelCase")]
623pub enum PlaneType {
624    #[serde(rename = "XY", alias = "xy")]
625    #[display("XY")]
626    XY,
627    #[serde(rename = "XZ", alias = "xz")]
628    #[display("XZ")]
629    XZ,
630    #[serde(rename = "YZ", alias = "yz")]
631    #[display("YZ")]
632    YZ,
633    /// A custom plane.
634    #[display("Custom")]
635    Custom,
636    /// A custom plane which has not been sent to the engine. It must be sent before it is used.
637    #[display("Uninit")]
638    Uninit,
639}
640
641#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
642#[ts(export)]
643#[serde(tag = "type", rename_all = "camelCase")]
644pub struct Sketch {
645    /// The id of the sketch (this will change when the engine's reference to it changes).
646    pub id: uuid::Uuid,
647    /// The paths in the sketch.
648    /// Only paths on the "outside" i.e. the perimeter.
649    /// Does not include paths "inside" the profile (for example, edges made by subtracting a profile)
650    pub paths: Vec<Path>,
651    /// Inner paths, resulting from subtract2d to carve profiles out of the sketch.
652    #[serde(default, skip_serializing_if = "Vec::is_empty")]
653    pub inner_paths: Vec<Path>,
654    /// What the sketch is on (can be a plane or a face).
655    pub on: SketchSurface,
656    /// The starting path.
657    pub start: BasePath,
658    /// Tag identifiers that have been declared in this sketch.
659    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
660    pub tags: IndexMap<String, TagIdentifier>,
661    /// The original id of the sketch. This stays the same even if the sketch is
662    /// is sketched on face etc.
663    pub artifact_id: ArtifactId,
664    #[ts(skip)]
665    pub original_id: uuid::Uuid,
666    /// If the sketch includes a mirror.
667    #[serde(skip)]
668    pub mirror: Option<uuid::Uuid>,
669    /// If the sketch is a clone of another sketch.
670    #[serde(skip)]
671    pub clone: Option<uuid::Uuid>,
672    pub units: UnitLength,
673    /// Metadata.
674    #[serde(skip)]
675    pub meta: Vec<Metadata>,
676    /// Has the profile been closed?
677    /// If not given, defaults to yes, closed explicitly.
678    #[serde(
679        default = "ProfileClosed::explicitly",
680        skip_serializing_if = "ProfileClosed::is_explicitly"
681    )]
682    pub is_closed: ProfileClosed,
683}
684
685impl ProfileClosed {
686    #[expect(dead_code, reason = "it's not actually dead, it's called by serde")]
687    fn explicitly() -> Self {
688        Self::Explicitly
689    }
690
691    fn is_explicitly(&self) -> bool {
692        matches!(self, ProfileClosed::Explicitly)
693    }
694}
695
696/// Has the profile been closed?
697#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd, ts_rs::TS)]
698#[serde(rename_all = "camelCase")]
699pub enum ProfileClosed {
700    /// It's definitely open.
701    No,
702    /// Unknown.
703    Maybe,
704    /// Yes, by adding a segment which loops back to the start.
705    Implicitly,
706    /// Yes, by calling `close()` or by making a closed shape (e.g. circle).
707    Explicitly,
708}
709
710impl Sketch {
711    // Tell the engine to enter sketch mode on the sketch.
712    // Run a specific command, then exit sketch mode.
713    pub(crate) fn build_sketch_mode_cmds(
714        &self,
715        exec_state: &mut ExecState,
716        inner_cmd: ModelingCmdReq,
717    ) -> Vec<ModelingCmdReq> {
718        vec![
719            // Before we extrude, we need to enable the sketch mode.
720            // We do this here in case extrude is called out of order.
721            ModelingCmdReq {
722                cmd: ModelingCmd::from(mcmd::EnableSketchMode {
723                    animated: false,
724                    ortho: false,
725                    entity_id: self.on.id(),
726                    adjust_camera: false,
727                    planar_normal: if let SketchSurface::Plane(plane) = &self.on {
728                        // We pass in the normal for the plane here.
729                        let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
730                        Some(normal.into())
731                    } else {
732                        None
733                    },
734                }),
735                cmd_id: exec_state.next_uuid().into(),
736            },
737            inner_cmd,
738            ModelingCmdReq {
739                cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
740                cmd_id: exec_state.next_uuid().into(),
741            },
742        ]
743    }
744}
745
746/// A sketch type.
747#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
748#[ts(export)]
749#[serde(tag = "type", rename_all = "camelCase")]
750pub enum SketchSurface {
751    Plane(Box<Plane>),
752    Face(Box<Face>),
753}
754
755impl SketchSurface {
756    pub(crate) fn id(&self) -> uuid::Uuid {
757        match self {
758            SketchSurface::Plane(plane) => plane.id,
759            SketchSurface::Face(face) => face.id,
760        }
761    }
762    pub(crate) fn x_axis(&self) -> Point3d {
763        match self {
764            SketchSurface::Plane(plane) => plane.info.x_axis,
765            SketchSurface::Face(face) => face.x_axis,
766        }
767    }
768    pub(crate) fn y_axis(&self) -> Point3d {
769        match self {
770            SketchSurface::Plane(plane) => plane.info.y_axis,
771            SketchSurface::Face(face) => face.y_axis,
772        }
773    }
774}
775
776#[derive(Debug, Clone)]
777pub(crate) enum GetTangentialInfoFromPathsResult {
778    PreviousPoint([f64; 2]),
779    Arc {
780        center: [f64; 2],
781        ccw: bool,
782    },
783    Circle {
784        center: [f64; 2],
785        ccw: bool,
786        radius: f64,
787    },
788    Ellipse {
789        center: [f64; 2],
790        ccw: bool,
791        major_axis: [f64; 2],
792        _minor_radius: f64,
793    },
794}
795
796impl GetTangentialInfoFromPathsResult {
797    pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
798        match self {
799            GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
800            GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
801                crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
802            }
803            // The circle always starts at 0 degrees, so a suitable tangent
804            // point is either directly above or below.
805            GetTangentialInfoFromPathsResult::Circle {
806                center, radius, ccw, ..
807            } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
808            GetTangentialInfoFromPathsResult::Ellipse {
809                center,
810                major_axis,
811                ccw,
812                ..
813            } => [center[0] + major_axis[0], center[1] + if *ccw { -1.0 } else { 1.0 }],
814        }
815    }
816}
817
818impl Sketch {
819    pub(crate) fn add_tag(
820        &mut self,
821        tag: NodeRef<'_, TagDeclarator>,
822        current_path: &Path,
823        exec_state: &ExecState,
824        surface: Option<&ExtrudeSurface>,
825    ) {
826        let mut tag_identifier: TagIdentifier = tag.into();
827        let base = current_path.get_base();
828        tag_identifier.info.push((
829            exec_state.stack().current_epoch(),
830            TagEngineInfo {
831                id: base.geo_meta.id,
832                sketch: self.id,
833                path: Some(current_path.clone()),
834                surface: surface.cloned(),
835            },
836        ));
837
838        self.tags.insert(tag.name.to_string(), tag_identifier);
839    }
840
841    pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
842        for t in tags {
843            match self.tags.get_mut(&t.value) {
844                Some(id) => {
845                    id.merge_info(t);
846                }
847                None => {
848                    self.tags.insert(t.value.clone(), t.clone());
849                }
850            }
851        }
852    }
853
854    /// Get the path most recently sketched.
855    pub(crate) fn latest_path(&self) -> Option<&Path> {
856        self.paths.last()
857    }
858
859    /// The "pen" is an imaginary pen drawing the path.
860    /// This gets the current point the pen is hovering over, i.e. the point
861    /// where the last path segment ends, and the next path segment will begin.
862    pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
863        let Some(path) = self.latest_path() else {
864            return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
865        };
866
867        let to = path.get_base().to;
868        Ok(Point2d::new(to[0], to[1], path.get_base().units))
869    }
870
871    pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
872        let Some(path) = self.latest_path() else {
873            return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
874        };
875        path.get_tangential_info()
876    }
877}
878
879#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
880#[ts(export)]
881#[serde(tag = "type", rename_all = "camelCase")]
882pub struct Solid {
883    /// The id of the solid.
884    pub id: uuid::Uuid,
885    /// The artifact ID of the solid.  Unlike `id`, this doesn't change.
886    pub artifact_id: ArtifactId,
887    /// The extrude surfaces.
888    pub value: Vec<ExtrudeSurface>,
889    /// The sketch.
890    pub sketch: Sketch,
891    /// The id of the extrusion start cap
892    pub start_cap_id: Option<uuid::Uuid>,
893    /// The id of the extrusion end cap
894    pub end_cap_id: Option<uuid::Uuid>,
895    /// Chamfers or fillets on this solid.
896    #[serde(default, skip_serializing_if = "Vec::is_empty")]
897    pub edge_cuts: Vec<EdgeCut>,
898    /// The units of the solid.
899    pub units: UnitLength,
900    /// Is this a sectional solid?
901    pub sectional: bool,
902    /// Metadata.
903    #[serde(skip)]
904    pub meta: Vec<Metadata>,
905}
906
907impl Solid {
908    pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
909        self.edge_cuts.iter().map(|foc| foc.id())
910    }
911}
912
913/// A fillet or a chamfer.
914#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
915#[ts(export)]
916#[serde(tag = "type", rename_all = "camelCase")]
917pub enum EdgeCut {
918    /// A fillet.
919    Fillet {
920        /// The id of the engine command that called this fillet.
921        id: uuid::Uuid,
922        radius: TyF64,
923        /// The engine id of the edge to fillet.
924        #[serde(rename = "edgeId")]
925        edge_id: uuid::Uuid,
926        tag: Box<Option<TagNode>>,
927    },
928    /// A chamfer.
929    Chamfer {
930        /// The id of the engine command that called this chamfer.
931        id: uuid::Uuid,
932        length: TyF64,
933        /// The engine id of the edge to chamfer.
934        #[serde(rename = "edgeId")]
935        edge_id: uuid::Uuid,
936        tag: Box<Option<TagNode>>,
937    },
938}
939
940impl EdgeCut {
941    pub fn id(&self) -> uuid::Uuid {
942        match self {
943            EdgeCut::Fillet { id, .. } => *id,
944            EdgeCut::Chamfer { id, .. } => *id,
945        }
946    }
947
948    pub fn set_id(&mut self, id: uuid::Uuid) {
949        match self {
950            EdgeCut::Fillet { id: i, .. } => *i = id,
951            EdgeCut::Chamfer { id: i, .. } => *i = id,
952        }
953    }
954
955    pub fn edge_id(&self) -> uuid::Uuid {
956        match self {
957            EdgeCut::Fillet { edge_id, .. } => *edge_id,
958            EdgeCut::Chamfer { edge_id, .. } => *edge_id,
959        }
960    }
961
962    pub fn set_edge_id(&mut self, id: uuid::Uuid) {
963        match self {
964            EdgeCut::Fillet { edge_id: i, .. } => *i = id,
965            EdgeCut::Chamfer { edge_id: i, .. } => *i = id,
966        }
967    }
968
969    pub fn tag(&self) -> Option<TagNode> {
970        match self {
971            EdgeCut::Fillet { tag, .. } => *tag.clone(),
972            EdgeCut::Chamfer { tag, .. } => *tag.clone(),
973        }
974    }
975}
976
977#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS)]
978#[ts(export)]
979pub struct Point2d {
980    pub x: f64,
981    pub y: f64,
982    pub units: UnitLength,
983}
984
985impl Point2d {
986    pub const ZERO: Self = Self {
987        x: 0.0,
988        y: 0.0,
989        units: UnitLength::Millimeters,
990    };
991
992    pub fn new(x: f64, y: f64, units: UnitLength) -> Self {
993        Self { x, y, units }
994    }
995
996    pub fn into_x(self) -> TyF64 {
997        TyF64::new(self.x, self.units.into())
998    }
999
1000    pub fn into_y(self) -> TyF64 {
1001        TyF64::new(self.y, self.units.into())
1002    }
1003
1004    pub fn ignore_units(self) -> [f64; 2] {
1005        [self.x, self.y]
1006    }
1007}
1008
1009#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, Default)]
1010#[ts(export)]
1011pub struct Point3d {
1012    pub x: f64,
1013    pub y: f64,
1014    pub z: f64,
1015    pub units: Option<UnitLength>,
1016}
1017
1018impl Point3d {
1019    pub const ZERO: Self = Self {
1020        x: 0.0,
1021        y: 0.0,
1022        z: 0.0,
1023        units: Some(UnitLength::Millimeters),
1024    };
1025
1026    pub fn new(x: f64, y: f64, z: f64, units: Option<UnitLength>) -> Self {
1027        Self { x, y, z, units }
1028    }
1029
1030    pub const fn is_zero(&self) -> bool {
1031        self.x == 0.0 && self.y == 0.0 && self.z == 0.0
1032    }
1033
1034    /// Calculate the cross product of this vector with another.
1035    ///
1036    /// This should only be applied to axes or other vectors which represent only a direction (and
1037    /// no magnitude) since units are ignored.
1038    pub fn axes_cross_product(&self, other: &Self) -> Self {
1039        Self {
1040            x: self.y * other.z - self.z * other.y,
1041            y: self.z * other.x - self.x * other.z,
1042            z: self.x * other.y - self.y * other.x,
1043            units: None,
1044        }
1045    }
1046
1047    /// Calculate the dot product of this vector with another.
1048    ///
1049    /// This should only be applied to axes or other vectors which represent only a direction (and
1050    /// no magnitude) since units are ignored.
1051    pub fn axes_dot_product(&self, other: &Self) -> f64 {
1052        let x = self.x * other.x;
1053        let y = self.y * other.y;
1054        let z = self.z * other.z;
1055        x + y + z
1056    }
1057
1058    pub fn normalize(&self) -> Self {
1059        let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
1060        Point3d {
1061            x: self.x / len,
1062            y: self.y / len,
1063            z: self.z / len,
1064            units: None,
1065        }
1066    }
1067
1068    pub fn as_3_dims(&self) -> ([f64; 3], Option<UnitLength>) {
1069        let p = [self.x, self.y, self.z];
1070        let u = self.units;
1071        (p, u)
1072    }
1073
1074    pub(crate) fn negated(self) -> Self {
1075        Self {
1076            x: -self.x,
1077            y: -self.y,
1078            z: -self.z,
1079            units: self.units,
1080        }
1081    }
1082}
1083
1084impl From<[TyF64; 3]> for Point3d {
1085    fn from(p: [TyF64; 3]) -> Self {
1086        Self {
1087            x: p[0].n,
1088            y: p[1].n,
1089            z: p[2].n,
1090            units: p[0].ty.as_length(),
1091        }
1092    }
1093}
1094
1095impl From<Point3d> for Point3D {
1096    fn from(p: Point3d) -> Self {
1097        Self { x: p.x, y: p.y, z: p.z }
1098    }
1099}
1100
1101impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
1102    fn from(p: Point3d) -> Self {
1103        if let Some(units) = p.units {
1104            Self {
1105                x: LengthUnit(adjust_length(units, p.x, UnitLength::Millimeters).0),
1106                y: LengthUnit(adjust_length(units, p.y, UnitLength::Millimeters).0),
1107                z: LengthUnit(adjust_length(units, p.z, UnitLength::Millimeters).0),
1108            }
1109        } else {
1110            Self {
1111                x: LengthUnit(p.x),
1112                y: LengthUnit(p.y),
1113                z: LengthUnit(p.z),
1114            }
1115        }
1116    }
1117}
1118
1119impl Add for Point3d {
1120    type Output = Point3d;
1121
1122    fn add(self, rhs: Self) -> Self::Output {
1123        // TODO should assert that self and rhs the same units or coerce them
1124        Point3d {
1125            x: self.x + rhs.x,
1126            y: self.y + rhs.y,
1127            z: self.z + rhs.z,
1128            units: self.units,
1129        }
1130    }
1131}
1132
1133impl AddAssign for Point3d {
1134    fn add_assign(&mut self, rhs: Self) {
1135        *self = *self + rhs
1136    }
1137}
1138
1139impl Sub for Point3d {
1140    type Output = Point3d;
1141
1142    fn sub(self, rhs: Self) -> Self::Output {
1143        let (x, y, z) = if rhs.units != self.units
1144            && let Some(sunits) = self.units
1145            && let Some(runits) = rhs.units
1146        {
1147            (
1148                adjust_length(runits, rhs.x, sunits).0,
1149                adjust_length(runits, rhs.y, sunits).0,
1150                adjust_length(runits, rhs.z, sunits).0,
1151            )
1152        } else {
1153            (rhs.x, rhs.y, rhs.z)
1154        };
1155        Point3d {
1156            x: self.x - x,
1157            y: self.y - y,
1158            z: self.z - z,
1159            units: self.units,
1160        }
1161    }
1162}
1163
1164impl SubAssign for Point3d {
1165    fn sub_assign(&mut self, rhs: Self) {
1166        *self = *self - rhs
1167    }
1168}
1169
1170impl Mul<f64> for Point3d {
1171    type Output = Point3d;
1172
1173    fn mul(self, rhs: f64) -> Self::Output {
1174        Point3d {
1175            x: self.x * rhs,
1176            y: self.y * rhs,
1177            z: self.z * rhs,
1178            units: self.units,
1179        }
1180    }
1181}
1182
1183/// A base path.
1184#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1185#[ts(export)]
1186#[serde(rename_all = "camelCase")]
1187pub struct BasePath {
1188    /// The from point.
1189    #[ts(type = "[number, number]")]
1190    pub from: [f64; 2],
1191    /// The to point.
1192    #[ts(type = "[number, number]")]
1193    pub to: [f64; 2],
1194    pub units: UnitLength,
1195    /// The tag of the path.
1196    pub tag: Option<TagNode>,
1197    /// Metadata.
1198    #[serde(rename = "__geoMeta")]
1199    pub geo_meta: GeoMeta,
1200}
1201
1202impl BasePath {
1203    pub fn get_to(&self) -> [TyF64; 2] {
1204        let ty: NumericType = self.units.into();
1205        [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1206    }
1207
1208    pub fn get_from(&self) -> [TyF64; 2] {
1209        let ty: NumericType = self.units.into();
1210        [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1211    }
1212}
1213
1214/// Geometry metadata.
1215#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1216#[ts(export)]
1217#[serde(rename_all = "camelCase")]
1218pub struct GeoMeta {
1219    /// The id of the geometry.
1220    pub id: uuid::Uuid,
1221    /// Metadata.
1222    #[serde(flatten)]
1223    pub metadata: Metadata,
1224}
1225
1226/// A path.
1227#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1228#[ts(export)]
1229#[serde(tag = "type")]
1230pub enum Path {
1231    /// A straight line which ends at the given point.
1232    ToPoint {
1233        #[serde(flatten)]
1234        base: BasePath,
1235    },
1236    /// A arc that is tangential to the last path segment that goes to a point
1237    TangentialArcTo {
1238        #[serde(flatten)]
1239        base: BasePath,
1240        /// the arc's center
1241        #[ts(type = "[number, number]")]
1242        center: [f64; 2],
1243        /// arc's direction
1244        ccw: bool,
1245    },
1246    /// A arc that is tangential to the last path segment
1247    TangentialArc {
1248        #[serde(flatten)]
1249        base: BasePath,
1250        /// the arc's center
1251        #[ts(type = "[number, number]")]
1252        center: [f64; 2],
1253        /// arc's direction
1254        ccw: bool,
1255    },
1256    // TODO: consolidate segment enums, remove Circle. https://github.com/KittyCAD/modeling-app/issues/3940
1257    /// a complete arc
1258    Circle {
1259        #[serde(flatten)]
1260        base: BasePath,
1261        /// the arc's center
1262        #[ts(type = "[number, number]")]
1263        center: [f64; 2],
1264        /// the arc's radius
1265        radius: f64,
1266        /// arc's direction
1267        /// This is used to compute the tangential angle.
1268        ccw: bool,
1269    },
1270    CircleThreePoint {
1271        #[serde(flatten)]
1272        base: BasePath,
1273        /// Point 1 of the circle
1274        #[ts(type = "[number, number]")]
1275        p1: [f64; 2],
1276        /// Point 2 of the circle
1277        #[ts(type = "[number, number]")]
1278        p2: [f64; 2],
1279        /// Point 3 of the circle
1280        #[ts(type = "[number, number]")]
1281        p3: [f64; 2],
1282    },
1283    ArcThreePoint {
1284        #[serde(flatten)]
1285        base: BasePath,
1286        /// Point 1 of the arc (base on the end of previous segment)
1287        #[ts(type = "[number, number]")]
1288        p1: [f64; 2],
1289        /// Point 2 of the arc (interiorAbsolute kwarg)
1290        #[ts(type = "[number, number]")]
1291        p2: [f64; 2],
1292        /// Point 3 of the arc (endAbsolute kwarg)
1293        #[ts(type = "[number, number]")]
1294        p3: [f64; 2],
1295    },
1296    /// A path that is horizontal.
1297    Horizontal {
1298        #[serde(flatten)]
1299        base: BasePath,
1300        /// The x coordinate.
1301        x: f64,
1302    },
1303    /// An angled line to.
1304    AngledLineTo {
1305        #[serde(flatten)]
1306        base: BasePath,
1307        /// The x coordinate.
1308        x: Option<f64>,
1309        /// The y coordinate.
1310        y: Option<f64>,
1311    },
1312    /// A base path.
1313    Base {
1314        #[serde(flatten)]
1315        base: BasePath,
1316    },
1317    /// A circular arc, not necessarily tangential to the current point.
1318    Arc {
1319        #[serde(flatten)]
1320        base: BasePath,
1321        /// Center of the circle that this arc is drawn on.
1322        center: [f64; 2],
1323        /// Radius of the circle that this arc is drawn on.
1324        radius: f64,
1325        /// True if the arc is counterclockwise.
1326        ccw: bool,
1327    },
1328    Ellipse {
1329        #[serde(flatten)]
1330        base: BasePath,
1331        center: [f64; 2],
1332        major_axis: [f64; 2],
1333        minor_radius: f64,
1334        ccw: bool,
1335    },
1336    //TODO: (bc) figure this out
1337    Conic {
1338        #[serde(flatten)]
1339        base: BasePath,
1340    },
1341}
1342
1343impl Path {
1344    pub fn get_id(&self) -> uuid::Uuid {
1345        match self {
1346            Path::ToPoint { base } => base.geo_meta.id,
1347            Path::Horizontal { base, .. } => base.geo_meta.id,
1348            Path::AngledLineTo { base, .. } => base.geo_meta.id,
1349            Path::Base { base } => base.geo_meta.id,
1350            Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1351            Path::TangentialArc { base, .. } => base.geo_meta.id,
1352            Path::Circle { base, .. } => base.geo_meta.id,
1353            Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1354            Path::Arc { base, .. } => base.geo_meta.id,
1355            Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1356            Path::Ellipse { base, .. } => base.geo_meta.id,
1357            Path::Conic { base, .. } => base.geo_meta.id,
1358        }
1359    }
1360
1361    pub fn set_id(&mut self, id: uuid::Uuid) {
1362        match self {
1363            Path::ToPoint { base } => base.geo_meta.id = id,
1364            Path::Horizontal { base, .. } => base.geo_meta.id = id,
1365            Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1366            Path::Base { base } => base.geo_meta.id = id,
1367            Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1368            Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1369            Path::Circle { base, .. } => base.geo_meta.id = id,
1370            Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1371            Path::Arc { base, .. } => base.geo_meta.id = id,
1372            Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1373            Path::Ellipse { base, .. } => base.geo_meta.id = id,
1374            Path::Conic { base, .. } => base.geo_meta.id = id,
1375        }
1376    }
1377
1378    pub fn get_tag(&self) -> Option<TagNode> {
1379        match self {
1380            Path::ToPoint { base } => base.tag.clone(),
1381            Path::Horizontal { base, .. } => base.tag.clone(),
1382            Path::AngledLineTo { base, .. } => base.tag.clone(),
1383            Path::Base { base } => base.tag.clone(),
1384            Path::TangentialArcTo { base, .. } => base.tag.clone(),
1385            Path::TangentialArc { base, .. } => base.tag.clone(),
1386            Path::Circle { base, .. } => base.tag.clone(),
1387            Path::CircleThreePoint { base, .. } => base.tag.clone(),
1388            Path::Arc { base, .. } => base.tag.clone(),
1389            Path::ArcThreePoint { base, .. } => base.tag.clone(),
1390            Path::Ellipse { base, .. } => base.tag.clone(),
1391            Path::Conic { base, .. } => base.tag.clone(),
1392        }
1393    }
1394
1395    pub fn get_base(&self) -> &BasePath {
1396        match self {
1397            Path::ToPoint { base } => base,
1398            Path::Horizontal { base, .. } => base,
1399            Path::AngledLineTo { base, .. } => base,
1400            Path::Base { base } => base,
1401            Path::TangentialArcTo { base, .. } => base,
1402            Path::TangentialArc { base, .. } => base,
1403            Path::Circle { base, .. } => base,
1404            Path::CircleThreePoint { base, .. } => base,
1405            Path::Arc { base, .. } => base,
1406            Path::ArcThreePoint { base, .. } => base,
1407            Path::Ellipse { base, .. } => base,
1408            Path::Conic { base, .. } => base,
1409        }
1410    }
1411
1412    /// Where does this path segment start?
1413    pub fn get_from(&self) -> [TyF64; 2] {
1414        let p = &self.get_base().from;
1415        let ty: NumericType = self.get_base().units.into();
1416        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1417    }
1418
1419    /// Where does this path segment end?
1420    pub fn get_to(&self) -> [TyF64; 2] {
1421        let p = &self.get_base().to;
1422        let ty: NumericType = self.get_base().units.into();
1423        [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1424    }
1425
1426    /// The path segment start point and its type.
1427    pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1428        let p = &self.get_base().from;
1429        let ty: NumericType = self.get_base().units.into();
1430        (*p, ty)
1431    }
1432
1433    /// The path segment end point and its type.
1434    pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1435        let p = &self.get_base().to;
1436        let ty: NumericType = self.get_base().units.into();
1437        (*p, ty)
1438    }
1439
1440    /// Length of this path segment, in cartesian plane. Not all segment types
1441    /// are supported.
1442    pub fn length(&self) -> Option<TyF64> {
1443        let n = match self {
1444            Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1445                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1446            }
1447            Self::TangentialArc {
1448                base: _,
1449                center,
1450                ccw: _,
1451            }
1452            | Self::TangentialArcTo {
1453                base: _,
1454                center,
1455                ccw: _,
1456            } => {
1457                // The radius can be calculated as the linear distance between `to` and `center`,
1458                // or between `from` and `center`. They should be the same.
1459                let radius = linear_distance(&self.get_base().from, center);
1460                debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1461                // TODO: Call engine utils to figure this out.
1462                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1463            }
1464            Self::Circle { radius, .. } => Some(2.0 * std::f64::consts::PI * radius),
1465            Self::CircleThreePoint { .. } => {
1466                let circle_center = crate::std::utils::calculate_circle_from_3_points([
1467                    self.get_base().from,
1468                    self.get_base().to,
1469                    self.get_base().to,
1470                ]);
1471                let radius = linear_distance(
1472                    &[circle_center.center[0], circle_center.center[1]],
1473                    &self.get_base().from,
1474                );
1475                Some(2.0 * std::f64::consts::PI * radius)
1476            }
1477            Self::Arc { .. } => {
1478                // TODO: Call engine utils to figure this out.
1479                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1480            }
1481            Self::ArcThreePoint { .. } => {
1482                // TODO: Call engine utils to figure this out.
1483                Some(linear_distance(&self.get_base().from, &self.get_base().to))
1484            }
1485            Self::Ellipse { .. } => {
1486                // Not supported.
1487                None
1488            }
1489            Self::Conic { .. } => {
1490                // Not supported.
1491                None
1492            }
1493        };
1494        n.map(|n| TyF64::new(n, self.get_base().units.into()))
1495    }
1496
1497    pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1498        match self {
1499            Path::ToPoint { base } => Some(base),
1500            Path::Horizontal { base, .. } => Some(base),
1501            Path::AngledLineTo { base, .. } => Some(base),
1502            Path::Base { base } => Some(base),
1503            Path::TangentialArcTo { base, .. } => Some(base),
1504            Path::TangentialArc { base, .. } => Some(base),
1505            Path::Circle { base, .. } => Some(base),
1506            Path::CircleThreePoint { base, .. } => Some(base),
1507            Path::Arc { base, .. } => Some(base),
1508            Path::ArcThreePoint { base, .. } => Some(base),
1509            Path::Ellipse { base, .. } => Some(base),
1510            Path::Conic { base, .. } => Some(base),
1511        }
1512    }
1513
1514    pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1515        match self {
1516            Path::TangentialArc { center, ccw, .. }
1517            | Path::TangentialArcTo { center, ccw, .. }
1518            | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1519                center: *center,
1520                ccw: *ccw,
1521            },
1522            Path::ArcThreePoint { p1, p2, p3, .. } => {
1523                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1524                GetTangentialInfoFromPathsResult::Arc {
1525                    center: circle.center,
1526                    ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1527                }
1528            }
1529            Path::Circle {
1530                center, ccw, radius, ..
1531            } => GetTangentialInfoFromPathsResult::Circle {
1532                center: *center,
1533                ccw: *ccw,
1534                radius: *radius,
1535            },
1536            Path::CircleThreePoint { p1, p2, p3, .. } => {
1537                let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1538                let center_point = [circle.center[0], circle.center[1]];
1539                GetTangentialInfoFromPathsResult::Circle {
1540                    center: center_point,
1541                    // Note: a circle is always ccw regardless of the order of points
1542                    ccw: true,
1543                    radius: circle.radius,
1544                }
1545            }
1546            // TODO: (bc) fix me
1547            Path::Ellipse {
1548                center,
1549                major_axis,
1550                minor_radius,
1551                ccw,
1552                ..
1553            } => GetTangentialInfoFromPathsResult::Ellipse {
1554                center: *center,
1555                major_axis: *major_axis,
1556                _minor_radius: *minor_radius,
1557                ccw: *ccw,
1558            },
1559            Path::Conic { .. }
1560            | Path::ToPoint { .. }
1561            | Path::Horizontal { .. }
1562            | Path::AngledLineTo { .. }
1563            | Path::Base { .. } => {
1564                let base = self.get_base();
1565                GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1566            }
1567        }
1568    }
1569
1570    /// i.e. not a curve
1571    pub(crate) fn is_straight_line(&self) -> bool {
1572        matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1573    }
1574}
1575
1576/// Compute the straight-line distance between a pair of (2D) points.
1577#[rustfmt::skip]
1578fn linear_distance(
1579    [x0, y0]: &[f64; 2],
1580    [x1, y1]: &[f64; 2]
1581) -> f64 {
1582    let y_sq = (y1 - y0).powi(2);
1583    let x_sq = (x1 - x0).powi(2);
1584    (y_sq + x_sq).sqrt()
1585}
1586
1587/// An extrude surface.
1588#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1589#[ts(export)]
1590#[serde(tag = "type", rename_all = "camelCase")]
1591pub enum ExtrudeSurface {
1592    /// An extrude plane.
1593    ExtrudePlane(ExtrudePlane),
1594    ExtrudeArc(ExtrudeArc),
1595    Chamfer(ChamferSurface),
1596    Fillet(FilletSurface),
1597}
1598
1599// Chamfer surface.
1600#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1601#[ts(export)]
1602#[serde(rename_all = "camelCase")]
1603pub struct ChamferSurface {
1604    /// The id for the chamfer surface.
1605    pub face_id: uuid::Uuid,
1606    /// The tag.
1607    pub tag: Option<Node<TagDeclarator>>,
1608    /// Metadata.
1609    #[serde(flatten)]
1610    pub geo_meta: GeoMeta,
1611}
1612
1613// Fillet surface.
1614#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1615#[ts(export)]
1616#[serde(rename_all = "camelCase")]
1617pub struct FilletSurface {
1618    /// The id for the fillet surface.
1619    pub face_id: uuid::Uuid,
1620    /// The tag.
1621    pub tag: Option<Node<TagDeclarator>>,
1622    /// Metadata.
1623    #[serde(flatten)]
1624    pub geo_meta: GeoMeta,
1625}
1626
1627/// An extruded plane.
1628#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1629#[ts(export)]
1630#[serde(rename_all = "camelCase")]
1631pub struct ExtrudePlane {
1632    /// The face id for the extrude plane.
1633    pub face_id: uuid::Uuid,
1634    /// The tag.
1635    pub tag: Option<Node<TagDeclarator>>,
1636    /// Metadata.
1637    #[serde(flatten)]
1638    pub geo_meta: GeoMeta,
1639}
1640
1641/// An extruded arc.
1642#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1643#[ts(export)]
1644#[serde(rename_all = "camelCase")]
1645pub struct ExtrudeArc {
1646    /// The face id for the extrude plane.
1647    pub face_id: uuid::Uuid,
1648    /// The tag.
1649    pub tag: Option<Node<TagDeclarator>>,
1650    /// Metadata.
1651    #[serde(flatten)]
1652    pub geo_meta: GeoMeta,
1653}
1654
1655impl ExtrudeSurface {
1656    pub fn get_id(&self) -> uuid::Uuid {
1657        match self {
1658            ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1659            ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1660            ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1661            ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1662        }
1663    }
1664
1665    pub fn face_id(&self) -> uuid::Uuid {
1666        match self {
1667            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id,
1668            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id,
1669            ExtrudeSurface::Fillet(f) => f.face_id,
1670            ExtrudeSurface::Chamfer(c) => c.face_id,
1671        }
1672    }
1673
1674    pub fn set_face_id(&mut self, face_id: uuid::Uuid) {
1675        match self {
1676            ExtrudeSurface::ExtrudePlane(ep) => ep.face_id = face_id,
1677            ExtrudeSurface::ExtrudeArc(ea) => ea.face_id = face_id,
1678            ExtrudeSurface::Fillet(f) => f.face_id = face_id,
1679            ExtrudeSurface::Chamfer(c) => c.face_id = face_id,
1680        }
1681    }
1682
1683    pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1684        match self {
1685            ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1686            ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1687            ExtrudeSurface::Fillet(f) => f.tag.clone(),
1688            ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1689        }
1690    }
1691}
1692
1693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, ts_rs::TS)]
1694pub struct SketchVarId(pub usize);
1695
1696impl SketchVarId {
1697    pub fn to_constraint_id(self, range: SourceRange) -> Result<kcl_ezpz::Id, KclError> {
1698        self.0.try_into().map_err(|_| {
1699            KclError::new_type(KclErrorDetails::new(
1700                "Cannot convert to constraint ID since the sketch variable ID is too large".to_owned(),
1701                vec![range],
1702            ))
1703        })
1704    }
1705}
1706
1707#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1708#[ts(export_to = "Geometry.ts")]
1709#[serde(rename_all = "camelCase")]
1710pub struct SketchVar {
1711    pub id: SketchVarId,
1712    pub initial_value: f64,
1713    pub ty: NumericType,
1714    #[serde(skip)]
1715    pub meta: Vec<Metadata>,
1716}
1717
1718impl SketchVar {
1719    pub fn initial_value_to_solver_units(
1720        &self,
1721        exec_state: &mut ExecState,
1722        source_range: SourceRange,
1723        description: &str,
1724    ) -> Result<TyF64, KclError> {
1725        let x_initial_value = KclValue::Number {
1726            value: self.initial_value,
1727            ty: self.ty,
1728            meta: vec![source_range.into()],
1729        };
1730        let normalized_value = normalize_to_solver_unit(&x_initial_value, source_range, exec_state, description)?;
1731        normalized_value.as_ty_f64().ok_or_else(|| {
1732            let message = format!(
1733                "Expected number after coercion, but found {}",
1734                normalized_value.human_friendly_type()
1735            );
1736            debug_assert!(false, "{}", &message);
1737            KclError::new_internal(KclErrorDetails::new(message, vec![source_range]))
1738        })
1739    }
1740}
1741
1742#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1743#[ts(export_to = "Geometry.ts")]
1744#[serde(tag = "type")]
1745pub enum UnsolvedExpr {
1746    Known(TyF64),
1747    Unknown(SketchVarId),
1748}
1749
1750impl UnsolvedExpr {
1751    pub fn var(&self) -> Option<SketchVarId> {
1752        match self {
1753            UnsolvedExpr::Known(_) => None,
1754            UnsolvedExpr::Unknown(id) => Some(*id),
1755        }
1756    }
1757}
1758
1759pub type UnsolvedPoint2dExpr = [UnsolvedExpr; 2];
1760
1761#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1762#[ts(export_to = "Geometry.ts")]
1763#[serde(rename_all = "camelCase")]
1764pub struct ConstrainablePoint2d {
1765    pub vars: crate::front::Point2d<SketchVarId>,
1766    pub object_id: ObjectId,
1767}
1768
1769#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1770#[ts(export_to = "Geometry.ts")]
1771#[serde(rename_all = "camelCase")]
1772pub struct UnsolvedSegment {
1773    pub object_id: ObjectId,
1774    pub kind: UnsolvedSegmentKind,
1775    #[serde(skip)]
1776    pub meta: Vec<Metadata>,
1777}
1778
1779#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1780#[ts(export_to = "Geometry.ts")]
1781#[serde(rename_all = "camelCase")]
1782pub enum UnsolvedSegmentKind {
1783    Point {
1784        position: UnsolvedPoint2dExpr,
1785        ctor: Box<PointCtor>,
1786    },
1787    Line {
1788        start: UnsolvedPoint2dExpr,
1789        end: UnsolvedPoint2dExpr,
1790        ctor: Box<LineCtor>,
1791        start_object_id: ObjectId,
1792        end_object_id: ObjectId,
1793    },
1794    Arc {
1795        start: UnsolvedPoint2dExpr,
1796        end: UnsolvedPoint2dExpr,
1797        center: UnsolvedPoint2dExpr,
1798        ctor: Box<ArcCtor>,
1799        start_object_id: ObjectId,
1800        end_object_id: ObjectId,
1801        center_object_id: ObjectId,
1802    },
1803}
1804
1805#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1806#[ts(export_to = "Geometry.ts")]
1807#[serde(rename_all = "camelCase")]
1808pub struct Segment {
1809    pub object_id: ObjectId,
1810    pub kind: SegmentKind,
1811    #[serde(skip)]
1812    pub meta: Vec<Metadata>,
1813}
1814
1815#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1816#[ts(export_to = "Geometry.ts")]
1817#[serde(rename_all = "camelCase")]
1818pub enum SegmentKind {
1819    Point {
1820        position: [TyF64; 2],
1821        ctor: Box<PointCtor>,
1822        #[serde(skip_serializing_if = "Option::is_none")]
1823        freedom: Option<Freedom>,
1824    },
1825    Line {
1826        start: [TyF64; 2],
1827        end: [TyF64; 2],
1828        ctor: Box<LineCtor>,
1829        start_object_id: ObjectId,
1830        end_object_id: ObjectId,
1831        #[serde(skip_serializing_if = "Option::is_none")]
1832        start_freedom: Option<Freedom>,
1833        #[serde(skip_serializing_if = "Option::is_none")]
1834        end_freedom: Option<Freedom>,
1835    },
1836    Arc {
1837        start: [TyF64; 2],
1838        end: [TyF64; 2],
1839        center: [TyF64; 2],
1840        ctor: Box<ArcCtor>,
1841        start_object_id: ObjectId,
1842        end_object_id: ObjectId,
1843        center_object_id: ObjectId,
1844        #[serde(skip_serializing_if = "Option::is_none")]
1845        start_freedom: Option<Freedom>,
1846        #[serde(skip_serializing_if = "Option::is_none")]
1847        end_freedom: Option<Freedom>,
1848        #[serde(skip_serializing_if = "Option::is_none")]
1849        center_freedom: Option<Freedom>,
1850    },
1851}
1852
1853#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1854#[ts(export_to = "Geometry.ts")]
1855#[serde(rename_all = "camelCase")]
1856pub struct AbstractSegment {
1857    pub repr: SegmentRepr,
1858    #[serde(skip)]
1859    pub meta: Vec<Metadata>,
1860}
1861
1862#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1863pub enum SegmentRepr {
1864    Unsolved { segment: UnsolvedSegment },
1865    Solved { segment: Segment },
1866}
1867
1868#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1869#[ts(export_to = "Geometry.ts")]
1870#[serde(rename_all = "camelCase")]
1871pub struct SketchConstraint {
1872    pub kind: SketchConstraintKind,
1873    #[serde(skip)]
1874    pub meta: Vec<Metadata>,
1875}
1876
1877#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1878#[ts(export_to = "Geometry.ts")]
1879#[serde(rename_all = "camelCase")]
1880pub enum SketchConstraintKind {
1881    Distance { points: [ConstrainablePoint2d; 2] },
1882}