1use std::ops::{Add, AddAssign, Mul};
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds as kcmc;
6use kittycad_modeling_cmds::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq};
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 engine::{DEFAULT_PLANE_INFO, PlaneName},
13 errors::{KclError, KclErrorDetails},
14 execution::{
15 ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen, types::NumericType,
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")]
27#[allow(clippy::large_enum_variant)]
28pub enum Geometry {
29 Sketch(Sketch),
30 Solid(Solid),
31}
32
33impl Geometry {
34 pub fn id(&self) -> uuid::Uuid {
35 match self {
36 Geometry::Sketch(s) => s.id,
37 Geometry::Solid(e) => e.id,
38 }
39 }
40
41 pub fn original_id(&self) -> uuid::Uuid {
45 match self {
46 Geometry::Sketch(s) => s.original_id,
47 Geometry::Solid(e) => e.sketch.original_id,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
54#[ts(export)]
55#[serde(tag = "type")]
56#[allow(clippy::large_enum_variant)]
57pub enum GeometryWithImportedGeometry {
58 Sketch(Sketch),
59 Solid(Solid),
60 ImportedGeometry(Box<ImportedGeometry>),
61}
62
63impl GeometryWithImportedGeometry {
64 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
65 match self {
66 GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
67 GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
68 GeometryWithImportedGeometry::ImportedGeometry(i) => {
69 let id = i.id(ctx).await?;
70 Ok(id)
71 }
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
78#[ts(export)]
79#[serde(tag = "type")]
80#[allow(clippy::vec_box)]
81pub enum Geometries {
82 Sketches(Vec<Sketch>),
83 Solids(Vec<Solid>),
84}
85
86impl From<Geometry> for Geometries {
87 fn from(value: Geometry) -> Self {
88 match value {
89 Geometry::Sketch(x) => Self::Sketches(vec![x]),
90 Geometry::Solid(x) => Self::Solids(vec![x]),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
97#[ts(export)]
98#[serde(rename_all = "camelCase")]
99pub struct ImportedGeometry {
100 pub id: uuid::Uuid,
102 pub value: Vec<String>,
104 #[serde(skip)]
105 pub meta: Vec<Metadata>,
106 #[serde(skip)]
108 completed: bool,
109}
110
111impl ImportedGeometry {
112 pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
113 Self {
114 id,
115 value,
116 meta,
117 completed: false,
118 }
119 }
120
121 async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
122 if self.completed {
123 return Ok(());
124 }
125
126 ctx.engine
127 .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
128 .await?;
129
130 self.completed = true;
131
132 Ok(())
133 }
134
135 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
136 if !self.completed {
137 self.wait_for_finish(ctx).await?;
138 }
139
140 Ok(self.id)
141 }
142}
143
144#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
146#[ts(export)]
147#[serde(tag = "type", rename_all = "camelCase")]
148#[allow(clippy::vec_box)]
149pub enum SolidOrSketchOrImportedGeometry {
150 ImportedGeometry(Box<ImportedGeometry>),
151 SolidSet(Vec<Solid>),
152 SketchSet(Vec<Sketch>),
153}
154
155impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
156 fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
157 match value {
158 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
159 SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
160 if s.len() == 1 {
161 crate::execution::KclValue::Solid {
162 value: Box::new(s.pop().unwrap()),
163 }
164 } else {
165 crate::execution::KclValue::HomArray {
166 value: s
167 .into_iter()
168 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
169 .collect(),
170 ty: crate::execution::types::RuntimeType::solid(),
171 }
172 }
173 }
174 SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
175 if s.len() == 1 {
176 crate::execution::KclValue::Sketch {
177 value: Box::new(s.pop().unwrap()),
178 }
179 } else {
180 crate::execution::KclValue::HomArray {
181 value: s
182 .into_iter()
183 .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
184 .collect(),
185 ty: crate::execution::types::RuntimeType::sketch(),
186 }
187 }
188 }
189 }
190 }
191}
192
193impl SolidOrSketchOrImportedGeometry {
194 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
195 match self {
196 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
197 let id = s.id(ctx).await?;
198
199 Ok(vec![id])
200 }
201 SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
202 SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
203 }
204 }
205}
206
207#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
209#[ts(export)]
210#[serde(tag = "type", rename_all = "camelCase")]
211#[allow(clippy::vec_box)]
212pub enum SolidOrImportedGeometry {
213 ImportedGeometry(Box<ImportedGeometry>),
214 SolidSet(Vec<Solid>),
215}
216
217impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
218 fn from(value: SolidOrImportedGeometry) -> Self {
219 match value {
220 SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
221 SolidOrImportedGeometry::SolidSet(mut s) => {
222 if s.len() == 1 {
223 crate::execution::KclValue::Solid {
224 value: Box::new(s.pop().unwrap()),
225 }
226 } else {
227 crate::execution::KclValue::HomArray {
228 value: s
229 .into_iter()
230 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
231 .collect(),
232 ty: crate::execution::types::RuntimeType::solid(),
233 }
234 }
235 }
236 }
237 }
238}
239
240impl SolidOrImportedGeometry {
241 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
242 match self {
243 SolidOrImportedGeometry::ImportedGeometry(s) => {
244 let id = s.id(ctx).await?;
245
246 Ok(vec![id])
247 }
248 SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
249 }
250 }
251}
252
253#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
255#[ts(export)]
256#[serde(rename_all = "camelCase")]
257pub struct Helix {
258 pub value: uuid::Uuid,
260 pub artifact_id: ArtifactId,
262 pub revolutions: f64,
264 pub angle_start: f64,
266 pub ccw: bool,
268 pub cylinder_id: Option<uuid::Uuid>,
270 pub units: UnitLen,
271 #[serde(skip)]
272 pub meta: Vec<Metadata>,
273}
274
275#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
276#[ts(export)]
277#[serde(rename_all = "camelCase")]
278pub struct Plane {
279 pub id: uuid::Uuid,
281 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::new_internal(KclErrorDetails::new(
475 format!("PlaneData {value:?} not found"),
476 Default::default(),
477 )));
478 }
479 };
480
481 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
482 KclError::new_internal(KclErrorDetails::new(
483 format!("Plane {name} not found"),
484 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 artifact_id: id.into(),
512 info: PlaneInfo::try_from(value.clone())?,
513 value: value.into(),
514 meta: vec![],
515 })
516 }
517
518 pub fn is_standard(&self) -> bool {
520 !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
521 }
522}
523
524#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
526#[ts(export)]
527#[serde(rename_all = "camelCase")]
528pub struct Face {
529 pub id: uuid::Uuid,
531 pub artifact_id: ArtifactId,
533 pub value: String,
535 pub x_axis: Point3d,
537 pub y_axis: Point3d,
539 pub solid: Box<Solid>,
541 pub units: UnitLen,
542 #[serde(skip)]
543 pub meta: Vec<Metadata>,
544}
545
546#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
548#[ts(export)]
549#[display(style = "camelCase")]
550pub enum PlaneType {
551 #[serde(rename = "XY", alias = "xy")]
552 #[display("XY")]
553 XY,
554 #[serde(rename = "XZ", alias = "xz")]
555 #[display("XZ")]
556 XZ,
557 #[serde(rename = "YZ", alias = "yz")]
558 #[display("YZ")]
559 YZ,
560 #[display("Custom")]
562 Custom,
563 #[display("Uninit")]
565 Uninit,
566}
567
568#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
569#[ts(export)]
570#[serde(tag = "type", rename_all = "camelCase")]
571pub struct Sketch {
572 pub id: uuid::Uuid,
574 pub paths: Vec<Path>,
576 pub on: SketchSurface,
578 pub start: BasePath,
580 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
582 pub tags: IndexMap<String, TagIdentifier>,
583 pub artifact_id: ArtifactId,
586 #[ts(skip)]
587 pub original_id: uuid::Uuid,
588 #[serde(skip)]
590 pub mirror: Option<uuid::Uuid>,
591 pub units: UnitLen,
592 #[serde(skip)]
594 pub meta: Vec<Metadata>,
595}
596
597impl Sketch {
598 pub(crate) fn build_sketch_mode_cmds(
601 &self,
602 exec_state: &mut ExecState,
603 inner_cmd: ModelingCmdReq,
604 ) -> Vec<ModelingCmdReq> {
605 vec![
606 ModelingCmdReq {
609 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
610 animated: false,
611 ortho: false,
612 entity_id: self.on.id(),
613 adjust_camera: false,
614 planar_normal: if let SketchSurface::Plane(plane) = &self.on {
615 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
617 Some(normal.into())
618 } else {
619 None
620 },
621 }),
622 cmd_id: exec_state.next_uuid().into(),
623 },
624 inner_cmd,
625 ModelingCmdReq {
626 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
627 cmd_id: exec_state.next_uuid().into(),
628 },
629 ]
630 }
631}
632
633#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
635#[ts(export)]
636#[serde(tag = "type", rename_all = "camelCase")]
637pub enum SketchSurface {
638 Plane(Box<Plane>),
639 Face(Box<Face>),
640}
641
642impl SketchSurface {
643 pub(crate) fn id(&self) -> uuid::Uuid {
644 match self {
645 SketchSurface::Plane(plane) => plane.id,
646 SketchSurface::Face(face) => face.id,
647 }
648 }
649 pub(crate) fn x_axis(&self) -> Point3d {
650 match self {
651 SketchSurface::Plane(plane) => plane.info.x_axis,
652 SketchSurface::Face(face) => face.x_axis,
653 }
654 }
655 pub(crate) fn y_axis(&self) -> Point3d {
656 match self {
657 SketchSurface::Plane(plane) => plane.info.y_axis,
658 SketchSurface::Face(face) => face.y_axis,
659 }
660 }
661}
662
663#[derive(Debug, Clone)]
664pub(crate) enum GetTangentialInfoFromPathsResult {
665 PreviousPoint([f64; 2]),
666 Arc { center: [f64; 2], ccw: bool },
667 Circle { center: [f64; 2], ccw: bool, radius: f64 },
668}
669
670impl GetTangentialInfoFromPathsResult {
671 pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
672 match self {
673 GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
674 GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
675 crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
676 }
677 GetTangentialInfoFromPathsResult::Circle {
680 center, radius, ccw, ..
681 } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
682 }
683 }
684}
685
686impl Sketch {
687 pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
688 let mut tag_identifier: TagIdentifier = tag.into();
689 let base = current_path.get_base();
690 tag_identifier.info.push((
691 exec_state.stack().current_epoch(),
692 TagEngineInfo {
693 id: base.geo_meta.id,
694 sketch: self.id,
695 path: Some(current_path.clone()),
696 surface: None,
697 },
698 ));
699
700 self.tags.insert(tag.name.to_string(), tag_identifier);
701 }
702
703 pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
704 for t in tags {
705 match self.tags.get_mut(&t.value) {
706 Some(id) => {
707 id.merge_info(t);
708 }
709 None => {
710 self.tags.insert(t.value.clone(), t.clone());
711 }
712 }
713 }
714 }
715
716 pub(crate) fn latest_path(&self) -> Option<&Path> {
718 self.paths.last()
719 }
720
721 pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
725 let Some(path) = self.latest_path() else {
726 return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
727 };
728
729 let to = path.get_base().to;
730 Ok(Point2d::new(to[0], to[1], path.get_base().units))
731 }
732
733 pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
734 let Some(path) = self.latest_path() else {
735 return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
736 };
737 path.get_tangential_info()
738 }
739}
740
741#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
742#[ts(export)]
743#[serde(tag = "type", rename_all = "camelCase")]
744pub struct Solid {
745 pub id: uuid::Uuid,
747 pub artifact_id: ArtifactId,
749 pub value: Vec<ExtrudeSurface>,
751 pub sketch: Sketch,
753 pub height: f64,
755 pub start_cap_id: Option<uuid::Uuid>,
757 pub end_cap_id: Option<uuid::Uuid>,
759 #[serde(default, skip_serializing_if = "Vec::is_empty")]
761 pub edge_cuts: Vec<EdgeCut>,
762 pub units: UnitLen,
764 pub sectional: bool,
766 #[serde(skip)]
768 pub meta: Vec<Metadata>,
769}
770
771impl Solid {
772 pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
773 self.edge_cuts.iter().map(|foc| foc.id())
774 }
775
776 pub(crate) fn height_in_mm(&self) -> f64 {
777 self.units.adjust_to(self.height, UnitLen::Mm).0
778 }
779}
780
781#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
783#[ts(export)]
784#[serde(tag = "type", rename_all = "camelCase")]
785pub enum EdgeCut {
786 Fillet {
788 id: uuid::Uuid,
790 radius: TyF64,
791 #[serde(rename = "edgeId")]
793 edge_id: uuid::Uuid,
794 tag: Box<Option<TagNode>>,
795 },
796 Chamfer {
798 id: uuid::Uuid,
800 length: TyF64,
801 #[serde(rename = "edgeId")]
803 edge_id: uuid::Uuid,
804 tag: Box<Option<TagNode>>,
805 },
806}
807
808impl EdgeCut {
809 pub fn id(&self) -> uuid::Uuid {
810 match self {
811 EdgeCut::Fillet { id, .. } => *id,
812 EdgeCut::Chamfer { id, .. } => *id,
813 }
814 }
815
816 pub fn set_id(&mut self, id: uuid::Uuid) {
817 match self {
818 EdgeCut::Fillet { id: i, .. } => *i = id,
819 EdgeCut::Chamfer { id: i, .. } => *i = id,
820 }
821 }
822
823 pub fn edge_id(&self) -> uuid::Uuid {
824 match self {
825 EdgeCut::Fillet { edge_id, .. } => *edge_id,
826 EdgeCut::Chamfer { edge_id, .. } => *edge_id,
827 }
828 }
829
830 pub fn set_edge_id(&mut self, id: uuid::Uuid) {
831 match self {
832 EdgeCut::Fillet { edge_id: i, .. } => *i = id,
833 EdgeCut::Chamfer { edge_id: i, .. } => *i = id,
834 }
835 }
836
837 pub fn tag(&self) -> Option<TagNode> {
838 match self {
839 EdgeCut::Fillet { tag, .. } => *tag.clone(),
840 EdgeCut::Chamfer { tag, .. } => *tag.clone(),
841 }
842 }
843}
844
845#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
846#[ts(export)]
847pub struct Point2d {
848 pub x: f64,
849 pub y: f64,
850 pub units: UnitLen,
851}
852
853impl Point2d {
854 pub const ZERO: Self = Self {
855 x: 0.0,
856 y: 0.0,
857 units: UnitLen::Mm,
858 };
859
860 pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
861 Self { x, y, units }
862 }
863
864 pub fn into_x(self) -> TyF64 {
865 TyF64::new(self.x, self.units.into())
866 }
867
868 pub fn into_y(self) -> TyF64 {
869 TyF64::new(self.y, self.units.into())
870 }
871
872 pub fn ignore_units(self) -> [f64; 2] {
873 [self.x, self.y]
874 }
875}
876
877#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
878#[ts(export)]
879pub struct Point3d {
880 pub x: f64,
881 pub y: f64,
882 pub z: f64,
883 pub units: UnitLen,
884}
885
886impl Point3d {
887 pub const ZERO: Self = Self {
888 x: 0.0,
889 y: 0.0,
890 z: 0.0,
891 units: UnitLen::Mm,
892 };
893
894 pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
895 Self { x, y, z, units }
896 }
897
898 pub const fn is_zero(&self) -> bool {
899 self.x == 0.0 && self.y == 0.0 && self.z == 0.0
900 }
901
902 pub fn axes_cross_product(&self, other: &Self) -> Self {
907 Self {
908 x: self.y * other.z - self.z * other.y,
909 y: self.z * other.x - self.x * other.z,
910 z: self.x * other.y - self.y * other.x,
911 units: UnitLen::Unknown,
912 }
913 }
914
915 pub fn normalize(&self) -> Self {
916 let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
917 Point3d {
918 x: self.x / len,
919 y: self.y / len,
920 z: self.z / len,
921 units: UnitLen::Unknown,
922 }
923 }
924}
925
926impl From<[TyF64; 3]> for Point3d {
927 fn from(p: [TyF64; 3]) -> Self {
928 Self {
929 x: p[0].n,
930 y: p[1].n,
931 z: p[2].n,
932 units: p[0].ty.expect_length(),
933 }
934 }
935}
936
937impl From<Point3d> for Point3D {
938 fn from(p: Point3d) -> Self {
939 Self { x: p.x, y: p.y, z: p.z }
940 }
941}
942
943impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
944 fn from(p: Point3d) -> Self {
945 Self {
946 x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
947 y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
948 z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
949 }
950 }
951}
952
953impl Add for Point3d {
954 type Output = Point3d;
955
956 fn add(self, rhs: Self) -> Self::Output {
957 Point3d {
959 x: self.x + rhs.x,
960 y: self.y + rhs.y,
961 z: self.z + rhs.z,
962 units: self.units,
963 }
964 }
965}
966
967impl AddAssign for Point3d {
968 fn add_assign(&mut self, rhs: Self) {
969 *self = *self + rhs
970 }
971}
972
973impl Mul<f64> for Point3d {
974 type Output = Point3d;
975
976 fn mul(self, rhs: f64) -> Self::Output {
977 Point3d {
978 x: self.x * rhs,
979 y: self.y * rhs,
980 z: self.z * rhs,
981 units: self.units,
982 }
983 }
984}
985
986#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
988#[ts(export)]
989#[serde(rename_all = "camelCase")]
990pub struct BasePath {
991 #[ts(type = "[number, number]")]
993 pub from: [f64; 2],
994 #[ts(type = "[number, number]")]
996 pub to: [f64; 2],
997 pub units: UnitLen,
998 pub tag: Option<TagNode>,
1000 #[serde(rename = "__geoMeta")]
1002 pub geo_meta: GeoMeta,
1003}
1004
1005impl BasePath {
1006 pub fn get_to(&self) -> [TyF64; 2] {
1007 let ty: NumericType = self.units.into();
1008 [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1009 }
1010
1011 pub fn get_from(&self) -> [TyF64; 2] {
1012 let ty: NumericType = self.units.into();
1013 [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1014 }
1015}
1016
1017#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1019#[ts(export)]
1020#[serde(rename_all = "camelCase")]
1021pub struct GeoMeta {
1022 pub id: uuid::Uuid,
1024 #[serde(flatten)]
1026 pub metadata: Metadata,
1027}
1028
1029#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1031#[ts(export)]
1032#[serde(tag = "type")]
1033pub enum Path {
1034 ToPoint {
1036 #[serde(flatten)]
1037 base: BasePath,
1038 },
1039 TangentialArcTo {
1041 #[serde(flatten)]
1042 base: BasePath,
1043 #[ts(type = "[number, number]")]
1045 center: [f64; 2],
1046 ccw: bool,
1048 },
1049 TangentialArc {
1051 #[serde(flatten)]
1052 base: BasePath,
1053 #[ts(type = "[number, number]")]
1055 center: [f64; 2],
1056 ccw: bool,
1058 },
1059 Circle {
1062 #[serde(flatten)]
1063 base: BasePath,
1064 #[ts(type = "[number, number]")]
1066 center: [f64; 2],
1067 radius: f64,
1069 ccw: bool,
1072 },
1073 CircleThreePoint {
1074 #[serde(flatten)]
1075 base: BasePath,
1076 #[ts(type = "[number, number]")]
1078 p1: [f64; 2],
1079 #[ts(type = "[number, number]")]
1081 p2: [f64; 2],
1082 #[ts(type = "[number, number]")]
1084 p3: [f64; 2],
1085 },
1086 ArcThreePoint {
1087 #[serde(flatten)]
1088 base: BasePath,
1089 #[ts(type = "[number, number]")]
1091 p1: [f64; 2],
1092 #[ts(type = "[number, number]")]
1094 p2: [f64; 2],
1095 #[ts(type = "[number, number]")]
1097 p3: [f64; 2],
1098 },
1099 Horizontal {
1101 #[serde(flatten)]
1102 base: BasePath,
1103 x: f64,
1105 },
1106 AngledLineTo {
1108 #[serde(flatten)]
1109 base: BasePath,
1110 x: Option<f64>,
1112 y: Option<f64>,
1114 },
1115 Base {
1117 #[serde(flatten)]
1118 base: BasePath,
1119 },
1120 Arc {
1122 #[serde(flatten)]
1123 base: BasePath,
1124 center: [f64; 2],
1126 radius: f64,
1128 ccw: bool,
1130 },
1131}
1132
1133#[derive(Display)]
1135enum PathType {
1136 ToPoint,
1137 Base,
1138 TangentialArc,
1139 TangentialArcTo,
1140 Circle,
1141 CircleThreePoint,
1142 Horizontal,
1143 AngledLineTo,
1144 Arc,
1145}
1146
1147impl From<&Path> for PathType {
1148 fn from(value: &Path) -> Self {
1149 match value {
1150 Path::ToPoint { .. } => Self::ToPoint,
1151 Path::TangentialArcTo { .. } => Self::TangentialArcTo,
1152 Path::TangentialArc { .. } => Self::TangentialArc,
1153 Path::Circle { .. } => Self::Circle,
1154 Path::CircleThreePoint { .. } => Self::CircleThreePoint,
1155 Path::Horizontal { .. } => Self::Horizontal,
1156 Path::AngledLineTo { .. } => Self::AngledLineTo,
1157 Path::Base { .. } => Self::Base,
1158 Path::Arc { .. } => Self::Arc,
1159 Path::ArcThreePoint { .. } => Self::Arc,
1160 }
1161 }
1162}
1163
1164impl Path {
1165 pub fn get_id(&self) -> uuid::Uuid {
1166 match self {
1167 Path::ToPoint { base } => base.geo_meta.id,
1168 Path::Horizontal { base, .. } => base.geo_meta.id,
1169 Path::AngledLineTo { base, .. } => base.geo_meta.id,
1170 Path::Base { base } => base.geo_meta.id,
1171 Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1172 Path::TangentialArc { base, .. } => base.geo_meta.id,
1173 Path::Circle { base, .. } => base.geo_meta.id,
1174 Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1175 Path::Arc { base, .. } => base.geo_meta.id,
1176 Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1177 }
1178 }
1179
1180 pub fn set_id(&mut self, id: uuid::Uuid) {
1181 match self {
1182 Path::ToPoint { base } => base.geo_meta.id = id,
1183 Path::Horizontal { base, .. } => base.geo_meta.id = id,
1184 Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1185 Path::Base { base } => base.geo_meta.id = id,
1186 Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1187 Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1188 Path::Circle { base, .. } => base.geo_meta.id = id,
1189 Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1190 Path::Arc { base, .. } => base.geo_meta.id = id,
1191 Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1192 }
1193 }
1194
1195 pub fn get_tag(&self) -> Option<TagNode> {
1196 match self {
1197 Path::ToPoint { base } => base.tag.clone(),
1198 Path::Horizontal { base, .. } => base.tag.clone(),
1199 Path::AngledLineTo { base, .. } => base.tag.clone(),
1200 Path::Base { base } => base.tag.clone(),
1201 Path::TangentialArcTo { base, .. } => base.tag.clone(),
1202 Path::TangentialArc { base, .. } => base.tag.clone(),
1203 Path::Circle { base, .. } => base.tag.clone(),
1204 Path::CircleThreePoint { base, .. } => base.tag.clone(),
1205 Path::Arc { base, .. } => base.tag.clone(),
1206 Path::ArcThreePoint { base, .. } => base.tag.clone(),
1207 }
1208 }
1209
1210 pub fn get_base(&self) -> &BasePath {
1211 match self {
1212 Path::ToPoint { base } => base,
1213 Path::Horizontal { base, .. } => base,
1214 Path::AngledLineTo { base, .. } => base,
1215 Path::Base { base } => base,
1216 Path::TangentialArcTo { base, .. } => base,
1217 Path::TangentialArc { base, .. } => base,
1218 Path::Circle { base, .. } => base,
1219 Path::CircleThreePoint { base, .. } => base,
1220 Path::Arc { base, .. } => base,
1221 Path::ArcThreePoint { base, .. } => base,
1222 }
1223 }
1224
1225 pub fn get_from(&self) -> [TyF64; 2] {
1227 let p = &self.get_base().from;
1228 let ty: NumericType = self.get_base().units.into();
1229 [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1230 }
1231
1232 pub fn get_to(&self) -> [TyF64; 2] {
1234 let p = &self.get_base().to;
1235 let ty: NumericType = self.get_base().units.into();
1236 [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1237 }
1238
1239 pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1241 let p = &self.get_base().from;
1242 let ty: NumericType = self.get_base().units.into();
1243 (*p, ty)
1244 }
1245
1246 pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1248 let p = &self.get_base().to;
1249 let ty: NumericType = self.get_base().units.into();
1250 (*p, ty)
1251 }
1252
1253 pub fn length(&self) -> TyF64 {
1255 let n = match self {
1256 Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1257 linear_distance(&self.get_base().from, &self.get_base().to)
1258 }
1259 Self::TangentialArc {
1260 base: _,
1261 center,
1262 ccw: _,
1263 }
1264 | Self::TangentialArcTo {
1265 base: _,
1266 center,
1267 ccw: _,
1268 } => {
1269 let radius = linear_distance(&self.get_base().from, center);
1272 debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1273 linear_distance(&self.get_base().from, &self.get_base().to)
1275 }
1276 Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
1277 Self::CircleThreePoint { .. } => {
1278 let circle_center = crate::std::utils::calculate_circle_from_3_points([
1279 self.get_base().from,
1280 self.get_base().to,
1281 self.get_base().to,
1282 ]);
1283 let radius = linear_distance(
1284 &[circle_center.center[0], circle_center.center[1]],
1285 &self.get_base().from,
1286 );
1287 2.0 * std::f64::consts::PI * radius
1288 }
1289 Self::Arc { .. } => {
1290 linear_distance(&self.get_base().from, &self.get_base().to)
1292 }
1293 Self::ArcThreePoint { .. } => {
1294 linear_distance(&self.get_base().from, &self.get_base().to)
1296 }
1297 };
1298 TyF64::new(n, self.get_base().units.into())
1299 }
1300
1301 pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1302 match self {
1303 Path::ToPoint { base } => Some(base),
1304 Path::Horizontal { base, .. } => Some(base),
1305 Path::AngledLineTo { base, .. } => Some(base),
1306 Path::Base { base } => Some(base),
1307 Path::TangentialArcTo { base, .. } => Some(base),
1308 Path::TangentialArc { base, .. } => Some(base),
1309 Path::Circle { base, .. } => Some(base),
1310 Path::CircleThreePoint { base, .. } => Some(base),
1311 Path::Arc { base, .. } => Some(base),
1312 Path::ArcThreePoint { base, .. } => Some(base),
1313 }
1314 }
1315
1316 pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1317 match self {
1318 Path::TangentialArc { center, ccw, .. }
1319 | Path::TangentialArcTo { center, ccw, .. }
1320 | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1321 center: *center,
1322 ccw: *ccw,
1323 },
1324 Path::ArcThreePoint { p1, p2, p3, .. } => {
1325 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1326 GetTangentialInfoFromPathsResult::Arc {
1327 center: circle.center,
1328 ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1329 }
1330 }
1331 Path::Circle {
1332 center, ccw, radius, ..
1333 } => GetTangentialInfoFromPathsResult::Circle {
1334 center: *center,
1335 ccw: *ccw,
1336 radius: *radius,
1337 },
1338 Path::CircleThreePoint { p1, p2, p3, .. } => {
1339 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1340 let center_point = [circle.center[0], circle.center[1]];
1341 GetTangentialInfoFromPathsResult::Circle {
1342 center: center_point,
1343 ccw: true,
1345 radius: circle.radius,
1346 }
1347 }
1348 Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
1349 let base = self.get_base();
1350 GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1351 }
1352 }
1353 }
1354
1355 pub(crate) fn is_straight_line(&self) -> bool {
1357 matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1358 }
1359}
1360
1361#[rustfmt::skip]
1363fn linear_distance(
1364 [x0, y0]: &[f64; 2],
1365 [x1, y1]: &[f64; 2]
1366) -> f64 {
1367 let y_sq = (y1 - y0).powi(2);
1368 let x_sq = (x1 - x0).powi(2);
1369 (y_sq + x_sq).sqrt()
1370}
1371
1372#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1374#[ts(export)]
1375#[serde(tag = "type", rename_all = "camelCase")]
1376pub enum ExtrudeSurface {
1377 ExtrudePlane(ExtrudePlane),
1379 ExtrudeArc(ExtrudeArc),
1380 Chamfer(ChamferSurface),
1381 Fillet(FilletSurface),
1382}
1383
1384#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1386#[ts(export)]
1387#[serde(rename_all = "camelCase")]
1388pub struct ChamferSurface {
1389 pub face_id: uuid::Uuid,
1391 pub tag: Option<Node<TagDeclarator>>,
1393 #[serde(flatten)]
1395 pub geo_meta: GeoMeta,
1396}
1397
1398#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1400#[ts(export)]
1401#[serde(rename_all = "camelCase")]
1402pub struct FilletSurface {
1403 pub face_id: uuid::Uuid,
1405 pub tag: Option<Node<TagDeclarator>>,
1407 #[serde(flatten)]
1409 pub geo_meta: GeoMeta,
1410}
1411
1412#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1414#[ts(export)]
1415#[serde(rename_all = "camelCase")]
1416pub struct ExtrudePlane {
1417 pub face_id: uuid::Uuid,
1419 pub tag: Option<Node<TagDeclarator>>,
1421 #[serde(flatten)]
1423 pub geo_meta: GeoMeta,
1424}
1425
1426#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1428#[ts(export)]
1429#[serde(rename_all = "camelCase")]
1430pub struct ExtrudeArc {
1431 pub face_id: uuid::Uuid,
1433 pub tag: Option<Node<TagDeclarator>>,
1435 #[serde(flatten)]
1437 pub geo_meta: GeoMeta,
1438}
1439
1440impl ExtrudeSurface {
1441 pub fn get_id(&self) -> uuid::Uuid {
1442 match self {
1443 ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1444 ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1445 ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1446 ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1447 }
1448 }
1449
1450 pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1451 match self {
1452 ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1453 ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1454 ExtrudeSurface::Fillet(f) => f.tag.clone(),
1455 ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1456 }
1457 }
1458}