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
11use crate::{
12 engine::{PlaneName, DEFAULT_PLANE_INFO},
13 errors::{KclError, KclErrorDetails},
14 execution::{
15 types::NumericType, ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen,
16 },
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 pub artifact_id: ArtifactId,
260 pub revolutions: f64,
262 pub angle_start: f64,
264 pub ccw: bool,
266 pub cylinder_id: Option<uuid::Uuid>,
268 pub units: UnitLen,
269 #[serde(skip)]
270 pub meta: Vec<Metadata>,
271}
272
273#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
274#[ts(export)]
275#[serde(rename_all = "camelCase")]
276pub struct Plane {
277 pub id: uuid::Uuid,
279 pub artifact_id: ArtifactId,
281 pub value: PlaneType,
283 #[serde(flatten)]
285 pub info: PlaneInfo,
286 #[serde(skip)]
287 pub meta: Vec<Metadata>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
291#[ts(export)]
292#[serde(rename_all = "camelCase")]
293pub struct PlaneInfo {
294 pub origin: Point3d,
296 pub x_axis: Point3d,
298 pub y_axis: Point3d,
300}
301
302impl PlaneInfo {
303 pub(crate) fn into_plane_data(self) -> PlaneData {
304 if self.origin.is_zero() {
305 match self {
306 Self {
307 origin:
308 Point3d {
309 x: 0.0,
310 y: 0.0,
311 z: 0.0,
312 units: UnitLen::Mm,
313 },
314 x_axis:
315 Point3d {
316 x: 1.0,
317 y: 0.0,
318 z: 0.0,
319 units: _,
320 },
321 y_axis:
322 Point3d {
323 x: 0.0,
324 y: 1.0,
325 z: 0.0,
326 units: _,
327 },
328 } => return PlaneData::XY,
329 Self {
330 origin:
331 Point3d {
332 x: 0.0,
333 y: 0.0,
334 z: 0.0,
335 units: UnitLen::Mm,
336 },
337 x_axis:
338 Point3d {
339 x: -1.0,
340 y: 0.0,
341 z: 0.0,
342 units: _,
343 },
344 y_axis:
345 Point3d {
346 x: 0.0,
347 y: 1.0,
348 z: 0.0,
349 units: _,
350 },
351 } => return PlaneData::NegXY,
352 Self {
353 origin:
354 Point3d {
355 x: 0.0,
356 y: 0.0,
357 z: 0.0,
358 units: UnitLen::Mm,
359 },
360 x_axis:
361 Point3d {
362 x: 1.0,
363 y: 0.0,
364 z: 0.0,
365 units: _,
366 },
367 y_axis:
368 Point3d {
369 x: 0.0,
370 y: 0.0,
371 z: 1.0,
372 units: _,
373 },
374 } => return PlaneData::XZ,
375 Self {
376 origin:
377 Point3d {
378 x: 0.0,
379 y: 0.0,
380 z: 0.0,
381 units: UnitLen::Mm,
382 },
383 x_axis:
384 Point3d {
385 x: -1.0,
386 y: 0.0,
387 z: 0.0,
388 units: _,
389 },
390 y_axis:
391 Point3d {
392 x: 0.0,
393 y: 0.0,
394 z: 1.0,
395 units: _,
396 },
397 } => return PlaneData::NegXZ,
398 Self {
399 origin:
400 Point3d {
401 x: 0.0,
402 y: 0.0,
403 z: 0.0,
404 units: UnitLen::Mm,
405 },
406 x_axis:
407 Point3d {
408 x: 0.0,
409 y: 1.0,
410 z: 0.0,
411 units: _,
412 },
413 y_axis:
414 Point3d {
415 x: 0.0,
416 y: 0.0,
417 z: 1.0,
418 units: _,
419 },
420 } => return PlaneData::YZ,
421 Self {
422 origin:
423 Point3d {
424 x: 0.0,
425 y: 0.0,
426 z: 0.0,
427 units: UnitLen::Mm,
428 },
429 x_axis:
430 Point3d {
431 x: 0.0,
432 y: -1.0,
433 z: 0.0,
434 units: _,
435 },
436 y_axis:
437 Point3d {
438 x: 0.0,
439 y: 0.0,
440 z: 1.0,
441 units: _,
442 },
443 } => return PlaneData::NegYZ,
444 _ => {}
445 }
446 }
447
448 PlaneData::Plane(Self {
449 origin: self.origin,
450 x_axis: self.x_axis,
451 y_axis: self.y_axis,
452 })
453 }
454}
455
456impl TryFrom<PlaneData> for PlaneInfo {
457 type Error = KclError;
458
459 fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
460 if let PlaneData::Plane(info) = value {
461 return Ok(info);
462 }
463 let name = match value {
464 PlaneData::XY => PlaneName::Xy,
465 PlaneData::NegXY => PlaneName::NegXy,
466 PlaneData::XZ => PlaneName::Xz,
467 PlaneData::NegXZ => PlaneName::NegXz,
468 PlaneData::YZ => PlaneName::Yz,
469 PlaneData::NegYZ => PlaneName::NegYz,
470 PlaneData::Plane(_) => {
471 return Err(KclError::Internal(KclErrorDetails::new(
473 format!("PlaneData {:?} not found", value),
474 Default::default(),
475 )));
476 }
477 };
478
479 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
480 KclError::Internal(KclErrorDetails::new(
481 format!("Plane {} not found", name),
482 Default::default(),
483 ))
484 })?;
485
486 Ok(info.clone())
487 }
488}
489
490impl From<PlaneData> for PlaneType {
491 fn from(value: PlaneData) -> Self {
492 match value {
493 PlaneData::XY => PlaneType::XY,
494 PlaneData::NegXY => PlaneType::XY,
495 PlaneData::XZ => PlaneType::XZ,
496 PlaneData::NegXZ => PlaneType::XZ,
497 PlaneData::YZ => PlaneType::YZ,
498 PlaneData::NegYZ => PlaneType::YZ,
499 PlaneData::Plane(_) => PlaneType::Custom,
500 }
501 }
502}
503
504impl Plane {
505 pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
506 let id = exec_state.next_uuid();
507 Ok(Plane {
508 id,
509 artifact_id: id.into(),
510 info: PlaneInfo::try_from(value.clone())?,
511 value: value.into(),
512 meta: vec![],
513 })
514 }
515
516 pub fn is_standard(&self) -> bool {
518 !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
519 }
520}
521
522#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
524#[ts(export)]
525#[serde(rename_all = "camelCase")]
526pub struct Face {
527 pub id: uuid::Uuid,
529 pub artifact_id: ArtifactId,
531 pub value: String,
533 pub x_axis: Point3d,
535 pub y_axis: Point3d,
537 pub solid: Box<Solid>,
539 pub units: UnitLen,
540 #[serde(skip)]
541 pub meta: Vec<Metadata>,
542}
543
544#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
546#[ts(export)]
547#[display(style = "camelCase")]
548pub enum PlaneType {
549 #[serde(rename = "XY", alias = "xy")]
550 #[display("XY")]
551 XY,
552 #[serde(rename = "XZ", alias = "xz")]
553 #[display("XZ")]
554 XZ,
555 #[serde(rename = "YZ", alias = "yz")]
556 #[display("YZ")]
557 YZ,
558 #[display("Custom")]
560 Custom,
561 #[display("Uninit")]
563 Uninit,
564}
565
566#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
567#[ts(export)]
568#[serde(tag = "type", rename_all = "camelCase")]
569pub struct Sketch {
570 pub id: uuid::Uuid,
572 pub paths: Vec<Path>,
574 pub on: SketchSurface,
576 pub start: BasePath,
578 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
580 pub tags: IndexMap<String, TagIdentifier>,
581 pub artifact_id: ArtifactId,
584 #[ts(skip)]
585 pub original_id: uuid::Uuid,
586 #[serde(skip)]
588 pub mirror: Option<uuid::Uuid>,
589 pub units: UnitLen,
590 #[serde(skip)]
592 pub meta: Vec<Metadata>,
593}
594
595impl Sketch {
596 pub(crate) fn build_sketch_mode_cmds(
599 &self,
600 exec_state: &mut ExecState,
601 inner_cmd: ModelingCmdReq,
602 ) -> Vec<ModelingCmdReq> {
603 vec![
604 ModelingCmdReq {
607 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
608 animated: false,
609 ortho: false,
610 entity_id: self.on.id(),
611 adjust_camera: false,
612 planar_normal: if let SketchSurface::Plane(plane) = &self.on {
613 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
615 Some(normal.into())
616 } else {
617 None
618 },
619 }),
620 cmd_id: exec_state.next_uuid().into(),
621 },
622 inner_cmd,
623 ModelingCmdReq {
624 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
625 cmd_id: exec_state.next_uuid().into(),
626 },
627 ]
628 }
629}
630
631#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
633#[ts(export)]
634#[serde(tag = "type", rename_all = "camelCase")]
635pub enum SketchSurface {
636 Plane(Box<Plane>),
637 Face(Box<Face>),
638}
639
640impl SketchSurface {
641 pub(crate) fn id(&self) -> uuid::Uuid {
642 match self {
643 SketchSurface::Plane(plane) => plane.id,
644 SketchSurface::Face(face) => face.id,
645 }
646 }
647 pub(crate) fn x_axis(&self) -> Point3d {
648 match self {
649 SketchSurface::Plane(plane) => plane.info.x_axis,
650 SketchSurface::Face(face) => face.x_axis,
651 }
652 }
653 pub(crate) fn y_axis(&self) -> Point3d {
654 match self {
655 SketchSurface::Plane(plane) => plane.info.y_axis,
656 SketchSurface::Face(face) => face.y_axis,
657 }
658 }
659}
660
661#[derive(Debug, Clone)]
662pub(crate) enum GetTangentialInfoFromPathsResult {
663 PreviousPoint([f64; 2]),
664 Arc { center: [f64; 2], ccw: bool },
665 Circle { center: [f64; 2], ccw: bool, radius: f64 },
666}
667
668impl GetTangentialInfoFromPathsResult {
669 pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
670 match self {
671 GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
672 GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
673 crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
674 }
675 GetTangentialInfoFromPathsResult::Circle {
678 center, radius, ccw, ..
679 } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
680 }
681 }
682}
683
684impl Sketch {
685 pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
686 let mut tag_identifier: TagIdentifier = tag.into();
687 let base = current_path.get_base();
688 tag_identifier.info.push((
689 exec_state.stack().current_epoch(),
690 TagEngineInfo {
691 id: base.geo_meta.id,
692 sketch: self.id,
693 path: Some(current_path.clone()),
694 surface: None,
695 },
696 ));
697
698 self.tags.insert(tag.name.to_string(), tag_identifier);
699 }
700
701 pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
702 for t in tags {
703 match self.tags.get_mut(&t.value) {
704 Some(id) => {
705 id.merge_info(t);
706 }
707 None => {
708 self.tags.insert(t.value.clone(), t.clone());
709 }
710 }
711 }
712 }
713
714 pub(crate) fn latest_path(&self) -> Option<&Path> {
716 self.paths.last()
717 }
718
719 pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
723 let Some(path) = self.latest_path() else {
724 return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
725 };
726
727 let to = path.get_base().to;
728 Ok(Point2d::new(to[0], to[1], path.get_base().units))
729 }
730
731 pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
732 let Some(path) = self.latest_path() else {
733 return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
734 };
735 path.get_tangential_info()
736 }
737}
738
739#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
740#[ts(export)]
741#[serde(tag = "type", rename_all = "camelCase")]
742pub struct Solid {
743 pub id: uuid::Uuid,
745 pub artifact_id: ArtifactId,
747 pub value: Vec<ExtrudeSurface>,
749 pub sketch: Sketch,
751 pub height: f64,
753 pub start_cap_id: Option<uuid::Uuid>,
755 pub end_cap_id: Option<uuid::Uuid>,
757 #[serde(default, skip_serializing_if = "Vec::is_empty")]
759 pub edge_cuts: Vec<EdgeCut>,
760 pub units: UnitLen,
762 pub sectional: bool,
764 #[serde(skip)]
766 pub meta: Vec<Metadata>,
767}
768
769impl Solid {
770 pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
771 self.edge_cuts.iter().map(|foc| foc.id())
772 }
773
774 pub(crate) fn height_in_mm(&self) -> f64 {
775 self.units.adjust_to(self.height, UnitLen::Mm).0
776 }
777}
778
779#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
781#[ts(export)]
782#[serde(tag = "type", rename_all = "camelCase")]
783pub enum EdgeCut {
784 Fillet {
786 id: uuid::Uuid,
788 radius: TyF64,
789 #[serde(rename = "edgeId")]
791 edge_id: uuid::Uuid,
792 tag: Box<Option<TagNode>>,
793 },
794 Chamfer {
796 id: uuid::Uuid,
798 length: TyF64,
799 #[serde(rename = "edgeId")]
801 edge_id: uuid::Uuid,
802 tag: Box<Option<TagNode>>,
803 },
804}
805
806impl EdgeCut {
807 pub fn id(&self) -> uuid::Uuid {
808 match self {
809 EdgeCut::Fillet { id, .. } => *id,
810 EdgeCut::Chamfer { id, .. } => *id,
811 }
812 }
813
814 pub fn set_id(&mut self, id: uuid::Uuid) {
815 match self {
816 EdgeCut::Fillet { id: ref mut i, .. } => *i = id,
817 EdgeCut::Chamfer { id: ref mut i, .. } => *i = id,
818 }
819 }
820
821 pub fn edge_id(&self) -> uuid::Uuid {
822 match self {
823 EdgeCut::Fillet { edge_id, .. } => *edge_id,
824 EdgeCut::Chamfer { edge_id, .. } => *edge_id,
825 }
826 }
827
828 pub fn set_edge_id(&mut self, id: uuid::Uuid) {
829 match self {
830 EdgeCut::Fillet { edge_id: ref mut i, .. } => *i = id,
831 EdgeCut::Chamfer { edge_id: ref mut i, .. } => *i = id,
832 }
833 }
834
835 pub fn tag(&self) -> Option<TagNode> {
836 match self {
837 EdgeCut::Fillet { tag, .. } => *tag.clone(),
838 EdgeCut::Chamfer { tag, .. } => *tag.clone(),
839 }
840 }
841}
842
843#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
844#[ts(export)]
845pub struct Point2d {
846 pub x: f64,
847 pub y: f64,
848 pub units: UnitLen,
849}
850
851impl Point2d {
852 pub const ZERO: Self = Self {
853 x: 0.0,
854 y: 0.0,
855 units: UnitLen::Mm,
856 };
857
858 pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
859 Self { x, y, units }
860 }
861
862 pub fn into_x(self) -> TyF64 {
863 TyF64::new(self.x, self.units.into())
864 }
865
866 pub fn into_y(self) -> TyF64 {
867 TyF64::new(self.y, self.units.into())
868 }
869
870 pub fn ignore_units(self) -> [f64; 2] {
871 [self.x, self.y]
872 }
873}
874
875#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
876#[ts(export)]
877pub struct Point3d {
878 pub x: f64,
879 pub y: f64,
880 pub z: f64,
881 pub units: UnitLen,
882}
883
884impl Point3d {
885 pub const ZERO: Self = Self {
886 x: 0.0,
887 y: 0.0,
888 z: 0.0,
889 units: UnitLen::Mm,
890 };
891
892 pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
893 Self { x, y, z, units }
894 }
895
896 pub const fn is_zero(&self) -> bool {
897 self.x == 0.0 && self.y == 0.0 && self.z == 0.0
898 }
899
900 pub fn axes_cross_product(&self, other: &Self) -> Self {
905 Self {
906 x: self.y * other.z - self.z * other.y,
907 y: self.z * other.x - self.x * other.z,
908 z: self.x * other.y - self.y * other.x,
909 units: UnitLen::Unknown,
910 }
911 }
912
913 pub fn normalize(&self) -> Self {
914 let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
915 Point3d {
916 x: self.x / len,
917 y: self.y / len,
918 z: self.z / len,
919 units: UnitLen::Unknown,
920 }
921 }
922}
923
924impl From<[TyF64; 3]> for Point3d {
925 fn from(p: [TyF64; 3]) -> Self {
926 Self {
927 x: p[0].n,
928 y: p[1].n,
929 z: p[2].n,
930 units: p[0].ty.expect_length(),
931 }
932 }
933}
934
935impl From<Point3d> for Point3D {
936 fn from(p: Point3d) -> Self {
937 Self { x: p.x, y: p.y, z: p.z }
938 }
939}
940impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
941 fn from(p: Point3d) -> Self {
942 Self {
943 x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
944 y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
945 z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
946 }
947 }
948}
949
950impl Add for Point3d {
951 type Output = Point3d;
952
953 fn add(self, rhs: Self) -> Self::Output {
954 Point3d {
956 x: self.x + rhs.x,
957 y: self.y + rhs.y,
958 z: self.z + rhs.z,
959 units: self.units,
960 }
961 }
962}
963
964impl AddAssign for Point3d {
965 fn add_assign(&mut self, rhs: Self) {
966 *self = *self + rhs
967 }
968}
969
970impl Mul<f64> for Point3d {
971 type Output = Point3d;
972
973 fn mul(self, rhs: f64) -> Self::Output {
974 Point3d {
975 x: self.x * rhs,
976 y: self.y * rhs,
977 z: self.z * rhs,
978 units: self.units,
979 }
980 }
981}
982
983#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
985#[ts(export)]
986#[serde(rename_all = "camelCase")]
987pub struct BasePath {
988 #[ts(type = "[number, number]")]
990 pub from: [f64; 2],
991 #[ts(type = "[number, number]")]
993 pub to: [f64; 2],
994 pub units: UnitLen,
995 pub tag: Option<TagNode>,
997 #[serde(rename = "__geoMeta")]
999 pub geo_meta: GeoMeta,
1000}
1001
1002impl BasePath {
1003 pub fn get_to(&self) -> [TyF64; 2] {
1004 let ty: NumericType = self.units.into();
1005 [TyF64::new(self.to[0], ty.clone()), TyF64::new(self.to[1], ty)]
1006 }
1007
1008 pub fn get_from(&self) -> [TyF64; 2] {
1009 let ty: NumericType = self.units.into();
1010 [TyF64::new(self.from[0], ty.clone()), TyF64::new(self.from[1], ty)]
1011 }
1012}
1013
1014#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1016#[ts(export)]
1017#[serde(rename_all = "camelCase")]
1018pub struct GeoMeta {
1019 pub id: uuid::Uuid,
1021 #[serde(flatten)]
1023 pub metadata: Metadata,
1024}
1025
1026#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1028#[ts(export)]
1029#[serde(tag = "type")]
1030pub enum Path {
1031 ToPoint {
1033 #[serde(flatten)]
1034 base: BasePath,
1035 },
1036 TangentialArcTo {
1038 #[serde(flatten)]
1039 base: BasePath,
1040 #[ts(type = "[number, number]")]
1042 center: [f64; 2],
1043 ccw: bool,
1045 },
1046 TangentialArc {
1048 #[serde(flatten)]
1049 base: BasePath,
1050 #[ts(type = "[number, number]")]
1052 center: [f64; 2],
1053 ccw: bool,
1055 },
1056 Circle {
1059 #[serde(flatten)]
1060 base: BasePath,
1061 #[ts(type = "[number, number]")]
1063 center: [f64; 2],
1064 radius: f64,
1066 ccw: bool,
1069 },
1070 CircleThreePoint {
1071 #[serde(flatten)]
1072 base: BasePath,
1073 #[ts(type = "[number, number]")]
1075 p1: [f64; 2],
1076 #[ts(type = "[number, number]")]
1078 p2: [f64; 2],
1079 #[ts(type = "[number, number]")]
1081 p3: [f64; 2],
1082 },
1083 ArcThreePoint {
1084 #[serde(flatten)]
1085 base: BasePath,
1086 #[ts(type = "[number, number]")]
1088 p1: [f64; 2],
1089 #[ts(type = "[number, number]")]
1091 p2: [f64; 2],
1092 #[ts(type = "[number, number]")]
1094 p3: [f64; 2],
1095 },
1096 Horizontal {
1098 #[serde(flatten)]
1099 base: BasePath,
1100 x: f64,
1102 },
1103 AngledLineTo {
1105 #[serde(flatten)]
1106 base: BasePath,
1107 x: Option<f64>,
1109 y: Option<f64>,
1111 },
1112 Base {
1114 #[serde(flatten)]
1115 base: BasePath,
1116 },
1117 Arc {
1119 #[serde(flatten)]
1120 base: BasePath,
1121 center: [f64; 2],
1123 radius: f64,
1125 ccw: bool,
1127 },
1128}
1129
1130#[derive(Display)]
1132enum PathType {
1133 ToPoint,
1134 Base,
1135 TangentialArc,
1136 TangentialArcTo,
1137 Circle,
1138 CircleThreePoint,
1139 Horizontal,
1140 AngledLineTo,
1141 Arc,
1142}
1143
1144impl From<&Path> for PathType {
1145 fn from(value: &Path) -> Self {
1146 match value {
1147 Path::ToPoint { .. } => Self::ToPoint,
1148 Path::TangentialArcTo { .. } => Self::TangentialArcTo,
1149 Path::TangentialArc { .. } => Self::TangentialArc,
1150 Path::Circle { .. } => Self::Circle,
1151 Path::CircleThreePoint { .. } => Self::CircleThreePoint,
1152 Path::Horizontal { .. } => Self::Horizontal,
1153 Path::AngledLineTo { .. } => Self::AngledLineTo,
1154 Path::Base { .. } => Self::Base,
1155 Path::Arc { .. } => Self::Arc,
1156 Path::ArcThreePoint { .. } => Self::Arc,
1157 }
1158 }
1159}
1160
1161impl Path {
1162 pub fn get_id(&self) -> uuid::Uuid {
1163 match self {
1164 Path::ToPoint { base } => base.geo_meta.id,
1165 Path::Horizontal { base, .. } => base.geo_meta.id,
1166 Path::AngledLineTo { base, .. } => base.geo_meta.id,
1167 Path::Base { base } => base.geo_meta.id,
1168 Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1169 Path::TangentialArc { base, .. } => base.geo_meta.id,
1170 Path::Circle { base, .. } => base.geo_meta.id,
1171 Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1172 Path::Arc { base, .. } => base.geo_meta.id,
1173 Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1174 }
1175 }
1176
1177 pub fn set_id(&mut self, id: uuid::Uuid) {
1178 match self {
1179 Path::ToPoint { base } => base.geo_meta.id = id,
1180 Path::Horizontal { base, .. } => base.geo_meta.id = id,
1181 Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1182 Path::Base { base } => base.geo_meta.id = id,
1183 Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1184 Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1185 Path::Circle { base, .. } => base.geo_meta.id = id,
1186 Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1187 Path::Arc { base, .. } => base.geo_meta.id = id,
1188 Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1189 }
1190 }
1191
1192 pub fn get_tag(&self) -> Option<TagNode> {
1193 match self {
1194 Path::ToPoint { base } => base.tag.clone(),
1195 Path::Horizontal { base, .. } => base.tag.clone(),
1196 Path::AngledLineTo { base, .. } => base.tag.clone(),
1197 Path::Base { base } => base.tag.clone(),
1198 Path::TangentialArcTo { base, .. } => base.tag.clone(),
1199 Path::TangentialArc { base, .. } => base.tag.clone(),
1200 Path::Circle { base, .. } => base.tag.clone(),
1201 Path::CircleThreePoint { base, .. } => base.tag.clone(),
1202 Path::Arc { base, .. } => base.tag.clone(),
1203 Path::ArcThreePoint { base, .. } => base.tag.clone(),
1204 }
1205 }
1206
1207 pub fn get_base(&self) -> &BasePath {
1208 match self {
1209 Path::ToPoint { base } => base,
1210 Path::Horizontal { base, .. } => base,
1211 Path::AngledLineTo { base, .. } => base,
1212 Path::Base { base } => base,
1213 Path::TangentialArcTo { base, .. } => base,
1214 Path::TangentialArc { base, .. } => base,
1215 Path::Circle { base, .. } => base,
1216 Path::CircleThreePoint { base, .. } => base,
1217 Path::Arc { base, .. } => base,
1218 Path::ArcThreePoint { base, .. } => base,
1219 }
1220 }
1221
1222 pub fn get_from(&self) -> [TyF64; 2] {
1224 let p = &self.get_base().from;
1225 let ty: NumericType = self.get_base().units.into();
1226 [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1227 }
1228
1229 pub fn get_to(&self) -> [TyF64; 2] {
1231 let p = &self.get_base().to;
1232 let ty: NumericType = self.get_base().units.into();
1233 [TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
1234 }
1235
1236 pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1238 let p = &self.get_base().from;
1239 let ty: NumericType = self.get_base().units.into();
1240 (*p, ty)
1241 }
1242
1243 pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1245 let p = &self.get_base().to;
1246 let ty: NumericType = self.get_base().units.into();
1247 (*p, ty)
1248 }
1249
1250 pub fn length(&self) -> TyF64 {
1252 let n = match self {
1253 Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1254 linear_distance(&self.get_base().from, &self.get_base().to)
1255 }
1256 Self::TangentialArc {
1257 base: _,
1258 center,
1259 ccw: _,
1260 }
1261 | Self::TangentialArcTo {
1262 base: _,
1263 center,
1264 ccw: _,
1265 } => {
1266 let radius = linear_distance(&self.get_base().from, center);
1269 debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1270 linear_distance(&self.get_base().from, &self.get_base().to)
1272 }
1273 Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1274 Self::CircleThreePoint { .. } => {
1275 let circle_center = crate::std::utils::calculate_circle_from_3_points([
1276 self.get_base().from,
1277 self.get_base().to,
1278 self.get_base().to,
1279 ]);
1280 let radius = linear_distance(
1281 &[circle_center.center[0], circle_center.center[1]],
1282 &self.get_base().from,
1283 );
1284 2.0 * std::f64::consts::PI * radius
1285 }
1286 Self::Arc { .. } => {
1287 linear_distance(&self.get_base().from, &self.get_base().to)
1289 }
1290 Self::ArcThreePoint { .. } => {
1291 linear_distance(&self.get_base().from, &self.get_base().to)
1293 }
1294 };
1295 TyF64::new(n, self.get_base().units.into())
1296 }
1297
1298 pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1299 match self {
1300 Path::ToPoint { base } => Some(base),
1301 Path::Horizontal { base, .. } => Some(base),
1302 Path::AngledLineTo { base, .. } => Some(base),
1303 Path::Base { base } => Some(base),
1304 Path::TangentialArcTo { base, .. } => Some(base),
1305 Path::TangentialArc { base, .. } => Some(base),
1306 Path::Circle { base, .. } => Some(base),
1307 Path::CircleThreePoint { base, .. } => Some(base),
1308 Path::Arc { base, .. } => Some(base),
1309 Path::ArcThreePoint { base, .. } => Some(base),
1310 }
1311 }
1312
1313 pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1314 match self {
1315 Path::TangentialArc { center, ccw, .. }
1316 | Path::TangentialArcTo { center, ccw, .. }
1317 | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1318 center: *center,
1319 ccw: *ccw,
1320 },
1321 Path::ArcThreePoint { p1, p2, p3, .. } => {
1322 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1323 GetTangentialInfoFromPathsResult::Arc {
1324 center: circle.center,
1325 ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1326 }
1327 }
1328 Path::Circle {
1329 center, ccw, radius, ..
1330 } => GetTangentialInfoFromPathsResult::Circle {
1331 center: *center,
1332 ccw: *ccw,
1333 radius: *radius,
1334 },
1335 Path::CircleThreePoint { p1, p2, p3, .. } => {
1336 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1337 let center_point = [circle.center[0], circle.center[1]];
1338 GetTangentialInfoFromPathsResult::Circle {
1339 center: center_point,
1340 ccw: true,
1342 radius: circle.radius,
1343 }
1344 }
1345 Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1346 let base = self.get_base();
1347 GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1348 }
1349 }
1350 }
1351}
1352
1353#[rustfmt::skip]
1355fn linear_distance(
1356 [x0, y0]: &[f64; 2],
1357 [x1, y1]: &[f64; 2]
1358) -> f64 {
1359 let y_sq = (y1 - y0).powi(2);
1360 let x_sq = (x1 - x0).powi(2);
1361 (y_sq + x_sq).sqrt()
1362}
1363
1364#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1366#[ts(export)]
1367#[serde(tag = "type", rename_all = "camelCase")]
1368pub enum ExtrudeSurface {
1369 ExtrudePlane(ExtrudePlane),
1371 ExtrudeArc(ExtrudeArc),
1372 Chamfer(ChamferSurface),
1373 Fillet(FilletSurface),
1374}
1375
1376#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1378#[ts(export)]
1379#[serde(rename_all = "camelCase")]
1380pub struct ChamferSurface {
1381 pub face_id: uuid::Uuid,
1383 pub tag: Option<Node<TagDeclarator>>,
1385 #[serde(flatten)]
1387 pub geo_meta: GeoMeta,
1388}
1389
1390#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1392#[ts(export)]
1393#[serde(rename_all = "camelCase")]
1394pub struct FilletSurface {
1395 pub face_id: uuid::Uuid,
1397 pub tag: Option<Node<TagDeclarator>>,
1399 #[serde(flatten)]
1401 pub geo_meta: GeoMeta,
1402}
1403
1404#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1406#[ts(export)]
1407#[serde(rename_all = "camelCase")]
1408pub struct ExtrudePlane {
1409 pub face_id: uuid::Uuid,
1411 pub tag: Option<Node<TagDeclarator>>,
1413 #[serde(flatten)]
1415 pub geo_meta: GeoMeta,
1416}
1417
1418#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1420#[ts(export)]
1421#[serde(rename_all = "camelCase")]
1422pub struct ExtrudeArc {
1423 pub face_id: uuid::Uuid,
1425 pub tag: Option<Node<TagDeclarator>>,
1427 #[serde(flatten)]
1429 pub geo_meta: GeoMeta,
1430}
1431
1432impl ExtrudeSurface {
1433 pub fn get_id(&self) -> uuid::Uuid {
1434 match self {
1435 ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1436 ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1437 ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1438 ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1439 }
1440 }
1441
1442 pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1443 match self {
1444 ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1445 ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1446 ExtrudeSurface::Fillet(f) => f.tag.clone(),
1447 ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1448 }
1449 }
1450}