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