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