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