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