1use std::ops::{Add, AddAssign, Mul};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds as kcmc;
6use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq, ModelingCmd};
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "artifact-graph")]
12use crate::execution::ArtifactId;
13use crate::{
14 engine::{PlaneName, DEFAULT_PLANE_INFO},
15 errors::{KclError, KclErrorDetails},
16 execution::{types::NumericType, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
17 parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
18 std::{args::TyF64, sketch::PlaneData},
19};
20
21type Point3D = kcmc::shared::Point3d<f64>;
22
23#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
25#[ts(export)]
26#[serde(tag = "type")]
27pub enum Geometry {
28 Sketch(Sketch),
29 Solid(Solid),
30}
31
32impl Geometry {
33 pub fn id(&self) -> uuid::Uuid {
34 match self {
35 Geometry::Sketch(s) => s.id,
36 Geometry::Solid(e) => e.id,
37 }
38 }
39
40 pub fn original_id(&self) -> uuid::Uuid {
44 match self {
45 Geometry::Sketch(s) => s.original_id,
46 Geometry::Solid(e) => e.sketch.original_id,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
53#[ts(export)]
54#[serde(tag = "type")]
55pub enum GeometryWithImportedGeometry {
56 Sketch(Sketch),
57 Solid(Solid),
58 ImportedGeometry(Box<ImportedGeometry>),
59}
60
61impl GeometryWithImportedGeometry {
62 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
63 match self {
64 GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
65 GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
66 GeometryWithImportedGeometry::ImportedGeometry(i) => {
67 let id = i.id(ctx).await?;
68 Ok(id)
69 }
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
76#[ts(export)]
77#[serde(tag = "type")]
78#[allow(clippy::vec_box)]
79pub enum Geometries {
80 Sketches(Vec<Sketch>),
81 Solids(Vec<Solid>),
82}
83
84impl From<Geometry> for Geometries {
85 fn from(value: Geometry) -> Self {
86 match value {
87 Geometry::Sketch(x) => Self::Sketches(vec![x]),
88 Geometry::Solid(x) => Self::Solids(vec![x]),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
95#[ts(export)]
96#[serde(rename_all = "camelCase")]
97pub struct ImportedGeometry {
98 pub id: uuid::Uuid,
100 pub value: Vec<String>,
102 #[serde(skip)]
103 pub meta: Vec<Metadata>,
104 #[serde(skip)]
106 completed: bool,
107}
108
109impl ImportedGeometry {
110 pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
111 Self {
112 id,
113 value,
114 meta,
115 completed: false,
116 }
117 }
118
119 async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
120 if self.completed {
121 return Ok(());
122 }
123
124 ctx.engine
125 .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
126 .await?;
127
128 self.completed = true;
129
130 Ok(())
131 }
132
133 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
134 if !self.completed {
135 self.wait_for_finish(ctx).await?;
136 }
137
138 Ok(self.id)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
144#[ts(export)]
145#[serde(tag = "type", rename_all = "camelCase")]
146#[allow(clippy::vec_box)]
147pub enum SolidOrSketchOrImportedGeometry {
148 ImportedGeometry(Box<ImportedGeometry>),
149 SolidSet(Vec<Solid>),
150 SketchSet(Vec<Sketch>),
151}
152
153impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
154 fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
155 match value {
156 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
157 SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
158 if s.len() == 1 {
159 crate::execution::KclValue::Solid {
160 value: Box::new(s.pop().unwrap()),
161 }
162 } else {
163 crate::execution::KclValue::HomArray {
164 value: s
165 .into_iter()
166 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
167 .collect(),
168 ty: crate::execution::types::RuntimeType::solid(),
169 }
170 }
171 }
172 SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
173 if s.len() == 1 {
174 crate::execution::KclValue::Sketch {
175 value: Box::new(s.pop().unwrap()),
176 }
177 } else {
178 crate::execution::KclValue::HomArray {
179 value: s
180 .into_iter()
181 .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
182 .collect(),
183 ty: crate::execution::types::RuntimeType::sketch(),
184 }
185 }
186 }
187 }
188 }
189}
190
191impl SolidOrSketchOrImportedGeometry {
192 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
193 match self {
194 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
195 let id = s.id(ctx).await?;
196
197 Ok(vec![id])
198 }
199 SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
200 SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
201 }
202 }
203}
204
205#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
207#[ts(export)]
208#[serde(tag = "type", rename_all = "camelCase")]
209#[allow(clippy::vec_box)]
210pub enum SolidOrImportedGeometry {
211 ImportedGeometry(Box<ImportedGeometry>),
212 SolidSet(Vec<Solid>),
213}
214
215impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
216 fn from(value: SolidOrImportedGeometry) -> Self {
217 match value {
218 SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
219 SolidOrImportedGeometry::SolidSet(mut s) => {
220 if s.len() == 1 {
221 crate::execution::KclValue::Solid {
222 value: Box::new(s.pop().unwrap()),
223 }
224 } else {
225 crate::execution::KclValue::HomArray {
226 value: s
227 .into_iter()
228 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
229 .collect(),
230 ty: crate::execution::types::RuntimeType::solid(),
231 }
232 }
233 }
234 }
235 }
236}
237
238impl SolidOrImportedGeometry {
239 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
240 match self {
241 SolidOrImportedGeometry::ImportedGeometry(s) => {
242 let id = s.id(ctx).await?;
243
244 Ok(vec![id])
245 }
246 SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
247 }
248 }
249}
250
251#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
253#[ts(export)]
254#[serde(rename_all = "camelCase")]
255pub struct Helix {
256 pub value: uuid::Uuid,
258 #[cfg(feature = "artifact-graph")]
260 pub artifact_id: ArtifactId,
261 pub revolutions: f64,
263 pub angle_start: f64,
265 pub ccw: bool,
267 pub cylinder_id: Option<uuid::Uuid>,
269 pub units: UnitLen,
270 #[serde(skip)]
271 pub meta: Vec<Metadata>,
272}
273
274#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
275#[ts(export)]
276#[serde(rename_all = "camelCase")]
277pub struct Plane {
278 pub id: uuid::Uuid,
280 #[cfg(feature = "artifact-graph")]
282 pub artifact_id: ArtifactId,
283 pub value: PlaneType,
285 #[serde(flatten)]
287 pub info: PlaneInfo,
288 #[serde(skip)]
289 pub meta: Vec<Metadata>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
293#[ts(export)]
294#[serde(rename_all = "camelCase")]
295pub struct PlaneInfo {
296 pub origin: Point3d,
298 pub x_axis: Point3d,
300 pub y_axis: Point3d,
302}
303
304impl PlaneInfo {
305 pub(crate) fn into_plane_data(self) -> PlaneData {
306 if self.origin.is_zero() {
307 match self {
308 Self {
309 origin:
310 Point3d {
311 x: 0.0,
312 y: 0.0,
313 z: 0.0,
314 units: UnitLen::Mm,
315 },
316 x_axis:
317 Point3d {
318 x: 1.0,
319 y: 0.0,
320 z: 0.0,
321 units: _,
322 },
323 y_axis:
324 Point3d {
325 x: 0.0,
326 y: 1.0,
327 z: 0.0,
328 units: _,
329 },
330 } => return PlaneData::XY,
331 Self {
332 origin:
333 Point3d {
334 x: 0.0,
335 y: 0.0,
336 z: 0.0,
337 units: UnitLen::Mm,
338 },
339 x_axis:
340 Point3d {
341 x: -1.0,
342 y: 0.0,
343 z: 0.0,
344 units: _,
345 },
346 y_axis:
347 Point3d {
348 x: 0.0,
349 y: 1.0,
350 z: 0.0,
351 units: _,
352 },
353 } => return PlaneData::NegXY,
354 Self {
355 origin:
356 Point3d {
357 x: 0.0,
358 y: 0.0,
359 z: 0.0,
360 units: UnitLen::Mm,
361 },
362 x_axis:
363 Point3d {
364 x: 1.0,
365 y: 0.0,
366 z: 0.0,
367 units: _,
368 },
369 y_axis:
370 Point3d {
371 x: 0.0,
372 y: 0.0,
373 z: 1.0,
374 units: _,
375 },
376 } => return PlaneData::XZ,
377 Self {
378 origin:
379 Point3d {
380 x: 0.0,
381 y: 0.0,
382 z: 0.0,
383 units: UnitLen::Mm,
384 },
385 x_axis:
386 Point3d {
387 x: -1.0,
388 y: 0.0,
389 z: 0.0,
390 units: _,
391 },
392 y_axis:
393 Point3d {
394 x: 0.0,
395 y: 0.0,
396 z: 1.0,
397 units: _,
398 },
399 } => return PlaneData::NegXZ,
400 Self {
401 origin:
402 Point3d {
403 x: 0.0,
404 y: 0.0,
405 z: 0.0,
406 units: UnitLen::Mm,
407 },
408 x_axis:
409 Point3d {
410 x: 0.0,
411 y: 1.0,
412 z: 0.0,
413 units: _,
414 },
415 y_axis:
416 Point3d {
417 x: 0.0,
418 y: 0.0,
419 z: 1.0,
420 units: _,
421 },
422 } => return PlaneData::YZ,
423 Self {
424 origin:
425 Point3d {
426 x: 0.0,
427 y: 0.0,
428 z: 0.0,
429 units: UnitLen::Mm,
430 },
431 x_axis:
432 Point3d {
433 x: 0.0,
434 y: -1.0,
435 z: 0.0,
436 units: _,
437 },
438 y_axis:
439 Point3d {
440 x: 0.0,
441 y: 0.0,
442 z: 1.0,
443 units: _,
444 },
445 } => return PlaneData::NegYZ,
446 _ => {}
447 }
448 }
449
450 PlaneData::Plane(Self {
451 origin: self.origin,
452 x_axis: self.x_axis,
453 y_axis: self.y_axis,
454 })
455 }
456}
457
458impl TryFrom<PlaneData> for PlaneInfo {
459 type Error = KclError;
460
461 fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
462 if let PlaneData::Plane(info) = value {
463 return Ok(info);
464 }
465 let name = match value {
466 PlaneData::XY => PlaneName::Xy,
467 PlaneData::NegXY => PlaneName::NegXy,
468 PlaneData::XZ => PlaneName::Xz,
469 PlaneData::NegXZ => PlaneName::NegXz,
470 PlaneData::YZ => PlaneName::Yz,
471 PlaneData::NegYZ => PlaneName::NegYz,
472 PlaneData::Plane(_) => {
473 return Err(KclError::Internal(KclErrorDetails {
475 message: format!("PlaneData {:?} not found", value),
476 source_ranges: Default::default(),
477 }));
478 }
479 };
480
481 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
482 KclError::Internal(KclErrorDetails {
483 message: format!("Plane {} not found", name),
484 source_ranges: Default::default(),
485 })
486 })?;
487
488 Ok(info.clone())
489 }
490}
491
492impl From<PlaneData> for PlaneType {
493 fn from(value: PlaneData) -> Self {
494 match value {
495 PlaneData::XY => PlaneType::XY,
496 PlaneData::NegXY => PlaneType::XY,
497 PlaneData::XZ => PlaneType::XZ,
498 PlaneData::NegXZ => PlaneType::XZ,
499 PlaneData::YZ => PlaneType::YZ,
500 PlaneData::NegYZ => PlaneType::YZ,
501 PlaneData::Plane(_) => PlaneType::Custom,
502 }
503 }
504}
505
506impl Plane {
507 pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
508 let id = exec_state.next_uuid();
509 Ok(Plane {
510 id,
511 #[cfg(feature = "artifact-graph")]
512 artifact_id: id.into(),
513 info: PlaneInfo::try_from(value.clone())?,
514 value: value.into(),
515 meta: vec![],
516 })
517 }
518
519 pub fn is_standard(&self) -> bool {
521 !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
522 }
523}
524
525#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
527#[ts(export)]
528#[serde(rename_all = "camelCase")]
529pub struct Face {
530 pub id: uuid::Uuid,
532 #[cfg(feature = "artifact-graph")]
534 pub artifact_id: ArtifactId,
535 pub value: String,
537 pub x_axis: Point3d,
539 pub y_axis: Point3d,
541 pub solid: Box<Solid>,
543 pub units: UnitLen,
544 #[serde(skip)]
545 pub meta: Vec<Metadata>,
546}
547
548#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
550#[ts(export)]
551#[display(style = "camelCase")]
552pub enum PlaneType {
553 #[serde(rename = "XY", alias = "xy")]
554 #[display("XY")]
555 XY,
556 #[serde(rename = "XZ", alias = "xz")]
557 #[display("XZ")]
558 XZ,
559 #[serde(rename = "YZ", alias = "yz")]
560 #[display("YZ")]
561 YZ,
562 #[display("Custom")]
564 Custom,
565 #[display("Uninit")]
567 Uninit,
568}
569
570#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
571#[ts(export)]
572#[serde(tag = "type", rename_all = "camelCase")]
573pub struct Sketch {
574 pub id: uuid::Uuid,
576 pub paths: Vec<Path>,
578 pub on: SketchSurface,
580 pub start: BasePath,
582 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
584 pub tags: IndexMap<String, TagIdentifier>,
585 #[cfg(feature = "artifact-graph")]
588 pub artifact_id: ArtifactId,
589 #[ts(skip)]
590 pub original_id: uuid::Uuid,
591 #[serde(skip)]
593 pub mirror: Option<uuid::Uuid>,
594 pub units: UnitLen,
595 #[serde(skip)]
597 pub meta: Vec<Metadata>,
598}
599
600impl Sketch {
601 pub(crate) fn build_sketch_mode_cmds(
604 &self,
605 exec_state: &mut ExecState,
606 inner_cmd: ModelingCmdReq,
607 ) -> Vec<ModelingCmdReq> {
608 vec![
609 ModelingCmdReq {
612 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
613 animated: false,
614 ortho: false,
615 entity_id: self.on.id(),
616 adjust_camera: false,
617 planar_normal: if let SketchSurface::Plane(plane) = &self.on {
618 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
620 Some(normal.into())
621 } else {
622 None
623 },
624 }),
625 cmd_id: exec_state.next_uuid().into(),
626 },
627 inner_cmd,
628 ModelingCmdReq {
629 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
630 cmd_id: exec_state.next_uuid().into(),
631 },
632 ]
633 }
634}
635
636#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
638#[ts(export)]
639#[serde(tag = "type", rename_all = "camelCase")]
640pub enum SketchSurface {
641 Plane(Box<Plane>),
642 Face(Box<Face>),
643}
644
645impl SketchSurface {
646 pub(crate) fn id(&self) -> uuid::Uuid {
647 match self {
648 SketchSurface::Plane(plane) => plane.id,
649 SketchSurface::Face(face) => face.id,
650 }
651 }
652 pub(crate) fn x_axis(&self) -> Point3d {
653 match self {
654 SketchSurface::Plane(plane) => plane.info.x_axis,
655 SketchSurface::Face(face) => face.x_axis,
656 }
657 }
658 pub(crate) fn y_axis(&self) -> Point3d {
659 match self {
660 SketchSurface::Plane(plane) => plane.info.y_axis,
661 SketchSurface::Face(face) => face.y_axis,
662 }
663 }
664}
665
666#[derive(Debug, Clone)]
667pub(crate) enum GetTangentialInfoFromPathsResult {
668 PreviousPoint([f64; 2]),
669 Arc { center: [f64; 2], ccw: bool },
670 Circle { center: [f64; 2], ccw: bool, radius: f64 },
671}
672
673impl GetTangentialInfoFromPathsResult {
674 pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
675 match self {
676 GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
677 GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
678 crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
679 }
680 GetTangentialInfoFromPathsResult::Circle {
683 center, radius, ccw, ..
684 } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
685 }
686 }
687}
688
689impl Sketch {
690 pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
691 let mut tag_identifier: TagIdentifier = tag.into();
692 let base = current_path.get_base();
693 tag_identifier.info.push((
694 exec_state.stack().current_epoch(),
695 TagEngineInfo {
696 id: base.geo_meta.id,
697 sketch: self.id,
698 path: Some(current_path.clone()),
699 surface: None,
700 },
701 ));
702
703 self.tags.insert(tag.name.to_string(), tag_identifier);
704 }
705
706 pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
707 for t in tags {
708 match self.tags.get_mut(&t.value) {
709 Some(id) => {
710 id.merge_info(t);
711 }
712 None => {
713 self.tags.insert(t.value.clone(), t.clone());
714 }
715 }
716 }
717 }
718
719 pub(crate) fn latest_path(&self) -> Option<&Path> {
721 self.paths.last()
722 }
723
724 pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
728 let Some(path) = self.latest_path() else {
729 return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
730 };
731
732 let to = path.get_base().to;
733 Ok(Point2d::new(to[0], to[1], path.get_base().units))
734 }
735
736 pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
737 let Some(path) = self.latest_path() else {
738 return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
739 };
740 path.get_tangential_info()
741 }
742}
743
744#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
745#[ts(export)]
746#[serde(tag = "type", rename_all = "camelCase")]
747pub struct Solid {
748 pub id: uuid::Uuid,
750 #[cfg(feature = "artifact-graph")]
752 pub artifact_id: ArtifactId,
753 pub value: Vec<ExtrudeSurface>,
755 pub sketch: Sketch,
757 pub height: f64,
759 pub start_cap_id: Option<uuid::Uuid>,
761 pub end_cap_id: Option<uuid::Uuid>,
763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
765 pub edge_cuts: Vec<EdgeCut>,
766 pub units: UnitLen,
768 pub sectional: bool,
770 #[serde(skip)]
772 pub meta: Vec<Metadata>,
773}
774
775impl Solid {
776 pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
777 self.edge_cuts.iter().map(|foc| foc.id())
778 }
779
780 pub(crate) fn height_in_mm(&self) -> f64 {
781 self.units.adjust_to(self.height, UnitLen::Mm).0
782 }
783}
784
785#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
787#[ts(export)]
788#[serde(tag = "type", rename_all = "camelCase")]
789pub enum EdgeCut {
790 Fillet {
792 id: uuid::Uuid,
794 radius: TyF64,
795 #[serde(rename = "edgeId")]
797 edge_id: uuid::Uuid,
798 tag: Box<Option<TagNode>>,
799 },
800 Chamfer {
802 id: uuid::Uuid,
804 length: TyF64,
805 #[serde(rename = "edgeId")]
807 edge_id: uuid::Uuid,
808 tag: Box<Option<TagNode>>,
809 },
810}
811
812impl EdgeCut {
813 pub fn id(&self) -> uuid::Uuid {
814 match self {
815 EdgeCut::Fillet { id, .. } => *id,
816 EdgeCut::Chamfer { id, .. } => *id,
817 }
818 }
819
820 pub fn set_id(&mut self, id: uuid::Uuid) {
821 match self {
822 EdgeCut::Fillet { id: ref mut i, .. } => *i = id,
823 EdgeCut::Chamfer { id: ref mut i, .. } => *i = id,
824 }
825 }
826
827 pub fn edge_id(&self) -> uuid::Uuid {
828 match self {
829 EdgeCut::Fillet { edge_id, .. } => *edge_id,
830 EdgeCut::Chamfer { edge_id, .. } => *edge_id,
831 }
832 }
833
834 pub fn set_edge_id(&mut self, id: uuid::Uuid) {
835 match self {
836 EdgeCut::Fillet { edge_id: ref mut i, .. } => *i = id,
837 EdgeCut::Chamfer { edge_id: ref mut i, .. } => *i = id,
838 }
839 }
840
841 pub fn tag(&self) -> Option<TagNode> {
842 match self {
843 EdgeCut::Fillet { tag, .. } => *tag.clone(),
844 EdgeCut::Chamfer { tag, .. } => *tag.clone(),
845 }
846 }
847}
848
849#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
850#[ts(export)]
851pub struct Point2d {
852 pub x: f64,
853 pub y: f64,
854 pub units: UnitLen,
855}
856
857impl Point2d {
858 pub const ZERO: Self = Self {
859 x: 0.0,
860 y: 0.0,
861 units: UnitLen::Mm,
862 };
863
864 pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
865 Self { x, y, units }
866 }
867
868 pub fn into_x(self) -> TyF64 {
869 TyF64::new(self.x, self.units.into())
870 }
871
872 pub fn into_y(self) -> TyF64 {
873 TyF64::new(self.y, self.units.into())
874 }
875
876 pub fn ignore_units(self) -> [f64; 2] {
877 [self.x, self.y]
878 }
879}
880
881#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
882#[ts(export)]
883pub struct Point3d {
884 pub x: f64,
885 pub y: f64,
886 pub z: f64,
887 pub units: UnitLen,
888}
889
890impl Point3d {
891 pub const ZERO: Self = Self {
892 x: 0.0,
893 y: 0.0,
894 z: 0.0,
895 units: UnitLen::Mm,
896 };
897
898 pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
899 Self { x, y, z, units }
900 }
901
902 pub const fn is_zero(&self) -> bool {
903 self.x == 0.0 && self.y == 0.0 && self.z == 0.0
904 }
905
906 pub fn axes_cross_product(&self, other: &Self) -> Self {
911 Self {
912 x: self.y * other.z - self.z * other.y,
913 y: self.z * other.x - self.x * other.z,
914 z: self.x * other.y - self.y * other.x,
915 units: UnitLen::Unknown,
916 }
917 }
918
919 pub fn normalize(&self) -> Self {
920 let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
921 Point3d {
922 x: self.x / len,
923 y: self.y / len,
924 z: self.z / len,
925 units: UnitLen::Unknown,
926 }
927 }
928}
929
930impl From<[TyF64; 3]> for Point3d {
931 fn from(p: [TyF64; 3]) -> Self {
932 Self {
933 x: p[0].n,
934 y: p[1].n,
935 z: p[2].n,
936 units: p[0].ty.expect_length(),
937 }
938 }
939}
940
941impl From<Point3d> for Point3D {
942 fn from(p: Point3d) -> Self {
943 Self { x: p.x, y: p.y, z: p.z }
944 }
945}
946impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
947 fn from(p: Point3d) -> Self {
948 Self {
949 x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
950 y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
951 z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
952 }
953 }
954}
955
956impl Add for Point3d {
957 type Output = Point3d;
958
959 fn add(self, rhs: Self) -> Self::Output {
960 Point3d {
962 x: self.x + rhs.x,
963 y: self.y + rhs.y,
964 z: self.z + rhs.z,
965 units: self.units,
966 }
967 }
968}
969
970impl AddAssign for Point3d {
971 fn add_assign(&mut self, rhs: Self) {
972 *self = *self + rhs
973 }
974}
975
976impl Mul<f64> for Point3d {
977 type Output = Point3d;
978
979 fn mul(self, rhs: f64) -> Self::Output {
980 Point3d {
981 x: self.x * rhs,
982 y: self.y * rhs,
983 z: self.z * rhs,
984 units: self.units,
985 }
986 }
987}
988
989#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
991#[ts(export)]
992#[serde(rename_all = "camelCase")]
993pub struct BasePath {
994 #[ts(type = "[number, number]")]
996 pub from: [f64; 2],
997 #[ts(type = "[number, number]")]
999 pub to: [f64; 2],
1000 pub units: UnitLen,
1001 pub tag: Option<TagNode>,
1003 #[serde(rename = "__geoMeta")]
1005 pub geo_meta: GeoMeta,
1006}
1007
1008impl BasePath {
1009 pub fn get_to(&self) -> [TyF64; 2] {
1010 let ty: NumericType = self.units.into();
1011 [TyF64::new(self.to[0], ty.clone()), TyF64::new(self.to[1], ty)]
1012 }
1013
1014 pub fn get_from(&self) -> [TyF64; 2] {
1015 let ty: NumericType = self.units.into();
1016 [TyF64::new(self.from[0], ty.clone()), TyF64::new(self.from[1], ty)]
1017 }
1018}
1019
1020#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1022#[ts(export)]
1023#[serde(rename_all = "camelCase")]
1024pub struct GeoMeta {
1025 pub id: uuid::Uuid,
1027 #[serde(flatten)]
1029 pub metadata: Metadata,
1030}
1031
1032#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1034#[ts(export)]
1035#[serde(tag = "type")]
1036pub enum Path {
1037 ToPoint {
1039 #[serde(flatten)]
1040 base: BasePath,
1041 },
1042 TangentialArcTo {
1044 #[serde(flatten)]
1045 base: BasePath,
1046 #[ts(type = "[number, number]")]
1048 center: [f64; 2],
1049 ccw: bool,
1051 },
1052 TangentialArc {
1054 #[serde(flatten)]
1055 base: BasePath,
1056 #[ts(type = "[number, number]")]
1058 center: [f64; 2],
1059 ccw: bool,
1061 },
1062 Circle {
1065 #[serde(flatten)]
1066 base: BasePath,
1067 #[ts(type = "[number, number]")]
1069 center: [f64; 2],
1070 radius: f64,
1072 ccw: bool,
1075 },
1076 CircleThreePoint {
1077 #[serde(flatten)]
1078 base: BasePath,
1079 #[ts(type = "[number, number]")]
1081 p1: [f64; 2],
1082 #[ts(type = "[number, number]")]
1084 p2: [f64; 2],
1085 #[ts(type = "[number, number]")]
1087 p3: [f64; 2],
1088 },
1089 ArcThreePoint {
1090 #[serde(flatten)]
1091 base: BasePath,
1092 #[ts(type = "[number, number]")]
1094 p1: [f64; 2],
1095 #[ts(type = "[number, number]")]
1097 p2: [f64; 2],
1098 #[ts(type = "[number, number]")]
1100 p3: [f64; 2],
1101 },
1102 Horizontal {
1104 #[serde(flatten)]
1105 base: BasePath,
1106 x: f64,
1108 },
1109 AngledLineTo {
1111 #[serde(flatten)]
1112 base: BasePath,
1113 x: Option<f64>,
1115 y: Option<f64>,
1117 },
1118 Base {
1120 #[serde(flatten)]
1121 base: BasePath,
1122 },
1123 Arc {
1125 #[serde(flatten)]
1126 base: BasePath,
1127 center: [f64; 2],
1129 radius: f64,
1131 ccw: bool,
1133 },
1134}
1135
1136#[derive(Display)]
1138enum PathType {
1139 ToPoint,
1140 Base,
1141 TangentialArc,
1142 TangentialArcTo,
1143 Circle,
1144 CircleThreePoint,
1145 Horizontal,
1146 AngledLineTo,
1147 Arc,
1148}
1149
1150impl From<&Path> for PathType {
1151 fn from(value: &Path) -> Self {
1152 match value {
1153 Path::ToPoint { .. } => Self::ToPoint,
1154 Path::TangentialArcTo { .. } => Self::TangentialArcTo,
1155 Path::TangentialArc { .. } => Self::TangentialArc,
1156 Path::Circle { .. } => Self::Circle,
1157 Path::CircleThreePoint { .. } => Self::CircleThreePoint,
1158 Path::Horizontal { .. } => Self::Horizontal,
1159 Path::AngledLineTo { .. } => Self::AngledLineTo,
1160 Path::Base { .. } => Self::Base,
1161 Path::Arc { .. } => Self::Arc,
1162 Path::ArcThreePoint { .. } => Self::Arc,
1163 }
1164 }
1165}
1166
1167impl Path {
1168 pub fn get_id(&self) -> uuid::Uuid {
1169 match self {
1170 Path::ToPoint { base } => base.geo_meta.id,
1171 Path::Horizontal { base, .. } => base.geo_meta.id,
1172 Path::AngledLineTo { base, .. } => base.geo_meta.id,
1173 Path::Base { base } => base.geo_meta.id,
1174 Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1175 Path::TangentialArc { base, .. } => base.geo_meta.id,
1176 Path::Circle { base, .. } => base.geo_meta.id,
1177 Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1178 Path::Arc { base, .. } => base.geo_meta.id,
1179 Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1180 }
1181 }
1182
1183 pub fn set_id(&mut self, id: uuid::Uuid) {
1184 match self {
1185 Path::ToPoint { base } => base.geo_meta.id = id,
1186 Path::Horizontal { base, .. } => base.geo_meta.id = id,
1187 Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1188 Path::Base { base } => base.geo_meta.id = id,
1189 Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1190 Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1191 Path::Circle { base, .. } => base.geo_meta.id = id,
1192 Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1193 Path::Arc { base, .. } => base.geo_meta.id = id,
1194 Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1195 }
1196 }
1197
1198 pub fn get_tag(&self) -> Option<TagNode> {
1199 match self {
1200 Path::ToPoint { base } => base.tag.clone(),
1201 Path::Horizontal { base, .. } => base.tag.clone(),
1202 Path::AngledLineTo { base, .. } => base.tag.clone(),
1203 Path::Base { base } => base.tag.clone(),
1204 Path::TangentialArcTo { base, .. } => base.tag.clone(),
1205 Path::TangentialArc { base, .. } => base.tag.clone(),
1206 Path::Circle { base, .. } => base.tag.clone(),
1207 Path::CircleThreePoint { base, .. } => base.tag.clone(),
1208 Path::Arc { base, .. } => base.tag.clone(),
1209 Path::ArcThreePoint { base, .. } => base.tag.clone(),
1210 }
1211 }
1212
1213 pub fn get_base(&self) -> &BasePath {
1214 match self {
1215 Path::ToPoint { base } => base,
1216 Path::Horizontal { base, .. } => base,
1217 Path::AngledLineTo { base, .. } => base,
1218 Path::Base { base } => base,
1219 Path::TangentialArcTo { base, .. } => base,
1220 Path::TangentialArc { base, .. } => base,
1221 Path::Circle { base, .. } => base,
1222 Path::CircleThreePoint { base, .. } => base,
1223 Path::Arc { base, .. } => base,
1224 Path::ArcThreePoint { base, .. } => base,
1225 }
1226 }
1227
1228 pub fn get_from(&self) -> [TyF64; 2] {
1230 let p = &self.get_base().from;
1231 let ty: NumericType = self.get_base().units.into();
1232 [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1233 }
1234
1235 pub fn get_to(&self) -> [TyF64; 2] {
1237 let p = &self.get_base().to;
1238 let ty: NumericType = self.get_base().units.into();
1239 [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1240 }
1241
1242 pub fn length(&self) -> TyF64 {
1244 let n = match self {
1245 Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1246 linear_distance(&self.get_base().from, &self.get_base().to)
1247 }
1248 Self::TangentialArc {
1249 base: _,
1250 center,
1251 ccw: _,
1252 }
1253 | Self::TangentialArcTo {
1254 base: _,
1255 center,
1256 ccw: _,
1257 } => {
1258 let radius = linear_distance(&self.get_base().from, center);
1261 debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1262 linear_distance(&self.get_base().from, &self.get_base().to)
1264 }
1265 Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1266 Self::CircleThreePoint { .. } => {
1267 let circle_center = crate::std::utils::calculate_circle_from_3_points([
1268 self.get_base().from,
1269 self.get_base().to,
1270 self.get_base().to,
1271 ]);
1272 let radius = linear_distance(
1273 &[circle_center.center[0], circle_center.center[1]],
1274 &self.get_base().from,
1275 );
1276 2.0 * std::f64::consts::PI * radius
1277 }
1278 Self::Arc { .. } => {
1279 linear_distance(&self.get_base().from, &self.get_base().to)
1281 }
1282 Self::ArcThreePoint { .. } => {
1283 linear_distance(&self.get_base().from, &self.get_base().to)
1285 }
1286 };
1287 TyF64::new(n, self.get_base().units.into())
1288 }
1289
1290 pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1291 match self {
1292 Path::ToPoint { base } => Some(base),
1293 Path::Horizontal { base, .. } => Some(base),
1294 Path::AngledLineTo { base, .. } => Some(base),
1295 Path::Base { base } => Some(base),
1296 Path::TangentialArcTo { base, .. } => Some(base),
1297 Path::TangentialArc { base, .. } => Some(base),
1298 Path::Circle { base, .. } => Some(base),
1299 Path::CircleThreePoint { base, .. } => Some(base),
1300 Path::Arc { base, .. } => Some(base),
1301 Path::ArcThreePoint { base, .. } => Some(base),
1302 }
1303 }
1304
1305 pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1306 match self {
1307 Path::TangentialArc { center, ccw, .. }
1308 | Path::TangentialArcTo { center, ccw, .. }
1309 | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1310 center: *center,
1311 ccw: *ccw,
1312 },
1313 Path::ArcThreePoint { p1, p2, p3, .. } => {
1314 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1315 GetTangentialInfoFromPathsResult::Arc {
1316 center: circle.center,
1317 ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1318 }
1319 }
1320 Path::Circle {
1321 center, ccw, radius, ..
1322 } => GetTangentialInfoFromPathsResult::Circle {
1323 center: *center,
1324 ccw: *ccw,
1325 radius: *radius,
1326 },
1327 Path::CircleThreePoint { p1, p2, p3, .. } => {
1328 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1329 let center_point = [circle.center[0], circle.center[1]];
1330 GetTangentialInfoFromPathsResult::Circle {
1331 center: center_point,
1332 ccw: true,
1334 radius: circle.radius,
1335 }
1336 }
1337 Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1338 let base = self.get_base();
1339 GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1340 }
1341 }
1342 }
1343}
1344
1345#[rustfmt::skip]
1347fn linear_distance(
1348 [x0, y0]: &[f64; 2],
1349 [x1, y1]: &[f64; 2]
1350) -> f64 {
1351 let y_sq = (y1 - y0).powi(2);
1352 let x_sq = (x1 - x0).powi(2);
1353 (y_sq + x_sq).sqrt()
1354}
1355
1356#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1358#[ts(export)]
1359#[serde(tag = "type", rename_all = "camelCase")]
1360pub enum ExtrudeSurface {
1361 ExtrudePlane(ExtrudePlane),
1363 ExtrudeArc(ExtrudeArc),
1364 Chamfer(ChamferSurface),
1365 Fillet(FilletSurface),
1366}
1367
1368#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1370#[ts(export)]
1371#[serde(rename_all = "camelCase")]
1372pub struct ChamferSurface {
1373 pub face_id: uuid::Uuid,
1375 pub tag: Option<Node<TagDeclarator>>,
1377 #[serde(flatten)]
1379 pub geo_meta: GeoMeta,
1380}
1381
1382#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1384#[ts(export)]
1385#[serde(rename_all = "camelCase")]
1386pub struct FilletSurface {
1387 pub face_id: uuid::Uuid,
1389 pub tag: Option<Node<TagDeclarator>>,
1391 #[serde(flatten)]
1393 pub geo_meta: GeoMeta,
1394}
1395
1396#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1398#[ts(export)]
1399#[serde(rename_all = "camelCase")]
1400pub struct ExtrudePlane {
1401 pub face_id: uuid::Uuid,
1403 pub tag: Option<Node<TagDeclarator>>,
1405 #[serde(flatten)]
1407 pub geo_meta: GeoMeta,
1408}
1409
1410#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1412#[ts(export)]
1413#[serde(rename_all = "camelCase")]
1414pub struct ExtrudeArc {
1415 pub face_id: uuid::Uuid,
1417 pub tag: Option<Node<TagDeclarator>>,
1419 #[serde(flatten)]
1421 pub geo_meta: GeoMeta,
1422}
1423
1424impl ExtrudeSurface {
1425 pub fn get_id(&self) -> uuid::Uuid {
1426 match self {
1427 ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1428 ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1429 ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1430 ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1431 }
1432 }
1433
1434 pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1435 match self {
1436 ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1437 ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1438 ExtrudeSurface::Fillet(f) => f.tag.clone(),
1439 ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1440 }
1441 }
1442}