Skip to main content

kcl_lib/execution/
geometry.rs

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