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