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