Skip to main content

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