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