Skip to main content

kcl_lib/execution/
geometry.rs

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