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