1use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
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 serde::{Deserialize, Serialize};
9
10use crate::{
11 engine::{DEFAULT_PLANE_INFO, PlaneName},
12 errors::{KclError, KclErrorDetails},
13 execution::{
14 ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen, types::NumericType,
15 },
16 parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
17 std::{args::TyF64, sketch::PlaneData},
18};
19
20type Point3D = kcmc::shared::Point3d<f64>;
21
22#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
24#[ts(export)]
25#[serde(tag = "type")]
26#[allow(clippy::large_enum_variant)]
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)]
53#[ts(export)]
54#[serde(tag = "type")]
55#[allow(clippy::large_enum_variant)]
56pub enum GeometryWithImportedGeometry {
57 Sketch(Sketch),
58 Solid(Solid),
59 ImportedGeometry(Box<ImportedGeometry>),
60}
61
62impl GeometryWithImportedGeometry {
63 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
64 match self {
65 GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
66 GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
67 GeometryWithImportedGeometry::ImportedGeometry(i) => {
68 let id = i.id(ctx).await?;
69 Ok(id)
70 }
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
77#[ts(export)]
78#[serde(tag = "type")]
79#[allow(clippy::vec_box)]
80pub enum Geometries {
81 Sketches(Vec<Sketch>),
82 Solids(Vec<Solid>),
83}
84
85impl From<Geometry> for Geometries {
86 fn from(value: Geometry) -> Self {
87 match value {
88 Geometry::Sketch(x) => Self::Sketches(vec![x]),
89 Geometry::Solid(x) => Self::Solids(vec![x]),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
96#[ts(export)]
97#[serde(rename_all = "camelCase")]
98pub struct ImportedGeometry {
99 pub id: uuid::Uuid,
101 pub value: Vec<String>,
103 #[serde(skip)]
104 pub meta: Vec<Metadata>,
105 #[serde(skip)]
107 completed: bool,
108}
109
110impl ImportedGeometry {
111 pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
112 Self {
113 id,
114 value,
115 meta,
116 completed: false,
117 }
118 }
119
120 async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
121 if self.completed {
122 return Ok(());
123 }
124
125 ctx.engine
126 .ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
127 .await?;
128
129 self.completed = true;
130
131 Ok(())
132 }
133
134 pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
135 if !self.completed {
136 self.wait_for_finish(ctx).await?;
137 }
138
139 Ok(self.id)
140 }
141}
142
143#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
145#[ts(export)]
146#[serde(tag = "type", rename_all = "camelCase")]
147#[allow(clippy::vec_box)]
148pub enum SolidOrSketchOrImportedGeometry {
149 ImportedGeometry(Box<ImportedGeometry>),
150 SolidSet(Vec<Solid>),
151 SketchSet(Vec<Sketch>),
152}
153
154impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
155 fn from(value: SolidOrSketchOrImportedGeometry) -> Self {
156 match value {
157 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
158 SolidOrSketchOrImportedGeometry::SolidSet(mut s) => {
159 if s.len() == 1 {
160 crate::execution::KclValue::Solid {
161 value: Box::new(s.pop().unwrap()),
162 }
163 } else {
164 crate::execution::KclValue::HomArray {
165 value: s
166 .into_iter()
167 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
168 .collect(),
169 ty: crate::execution::types::RuntimeType::solid(),
170 }
171 }
172 }
173 SolidOrSketchOrImportedGeometry::SketchSet(mut s) => {
174 if s.len() == 1 {
175 crate::execution::KclValue::Sketch {
176 value: Box::new(s.pop().unwrap()),
177 }
178 } else {
179 crate::execution::KclValue::HomArray {
180 value: s
181 .into_iter()
182 .map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
183 .collect(),
184 ty: crate::execution::types::RuntimeType::sketch(),
185 }
186 }
187 }
188 }
189 }
190}
191
192impl SolidOrSketchOrImportedGeometry {
193 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
194 match self {
195 SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
196 let id = s.id(ctx).await?;
197
198 Ok(vec![id])
199 }
200 SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
201 SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
208#[ts(export)]
209#[serde(tag = "type", rename_all = "camelCase")]
210#[allow(clippy::vec_box)]
211pub enum SolidOrImportedGeometry {
212 ImportedGeometry(Box<ImportedGeometry>),
213 SolidSet(Vec<Solid>),
214}
215
216impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
217 fn from(value: SolidOrImportedGeometry) -> Self {
218 match value {
219 SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
220 SolidOrImportedGeometry::SolidSet(mut s) => {
221 if s.len() == 1 {
222 crate::execution::KclValue::Solid {
223 value: Box::new(s.pop().unwrap()),
224 }
225 } else {
226 crate::execution::KclValue::HomArray {
227 value: s
228 .into_iter()
229 .map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
230 .collect(),
231 ty: crate::execution::types::RuntimeType::solid(),
232 }
233 }
234 }
235 }
236 }
237}
238
239impl SolidOrImportedGeometry {
240 pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
241 match self {
242 SolidOrImportedGeometry::ImportedGeometry(s) => {
243 let id = s.id(ctx).await?;
244
245 Ok(vec![id])
246 }
247 SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
254#[ts(export)]
255#[serde(rename_all = "camelCase")]
256pub struct Helix {
257 pub value: uuid::Uuid,
259 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)]
275#[ts(export)]
276#[serde(rename_all = "camelCase")]
277pub struct Plane {
278 pub id: uuid::Uuid,
280 pub artifact_id: ArtifactId,
282 pub value: PlaneType,
284 #[serde(flatten)]
286 pub info: PlaneInfo,
287 #[serde(skip)]
288 pub meta: Vec<Metadata>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
292#[ts(export)]
293#[serde(rename_all = "camelCase")]
294pub struct PlaneInfo {
295 pub origin: Point3d,
297 pub x_axis: Point3d,
299 pub y_axis: Point3d,
301 pub z_axis: Point3d,
303}
304
305impl PlaneInfo {
306 pub(crate) fn into_plane_data(self) -> PlaneData {
307 if self.origin.is_zero() {
308 match self {
309 Self {
310 origin:
311 Point3d {
312 x: 0.0,
313 y: 0.0,
314 z: 0.0,
315 units: UnitLen::Mm,
316 },
317 x_axis:
318 Point3d {
319 x: 1.0,
320 y: 0.0,
321 z: 0.0,
322 units: _,
323 },
324 y_axis:
325 Point3d {
326 x: 0.0,
327 y: 1.0,
328 z: 0.0,
329 units: _,
330 },
331 z_axis: _,
332 } => return PlaneData::XY,
333 Self {
334 origin:
335 Point3d {
336 x: 0.0,
337 y: 0.0,
338 z: 0.0,
339 units: UnitLen::Mm,
340 },
341 x_axis:
342 Point3d {
343 x: -1.0,
344 y: 0.0,
345 z: 0.0,
346 units: _,
347 },
348 y_axis:
349 Point3d {
350 x: 0.0,
351 y: 1.0,
352 z: 0.0,
353 units: _,
354 },
355 z_axis: _,
356 } => return PlaneData::NegXY,
357 Self {
358 origin:
359 Point3d {
360 x: 0.0,
361 y: 0.0,
362 z: 0.0,
363 units: UnitLen::Mm,
364 },
365 x_axis:
366 Point3d {
367 x: 1.0,
368 y: 0.0,
369 z: 0.0,
370 units: _,
371 },
372 y_axis:
373 Point3d {
374 x: 0.0,
375 y: 0.0,
376 z: 1.0,
377 units: _,
378 },
379 z_axis: _,
380 } => return PlaneData::XZ,
381 Self {
382 origin:
383 Point3d {
384 x: 0.0,
385 y: 0.0,
386 z: 0.0,
387 units: UnitLen::Mm,
388 },
389 x_axis:
390 Point3d {
391 x: -1.0,
392 y: 0.0,
393 z: 0.0,
394 units: _,
395 },
396 y_axis:
397 Point3d {
398 x: 0.0,
399 y: 0.0,
400 z: 1.0,
401 units: _,
402 },
403 z_axis: _,
404 } => return PlaneData::NegXZ,
405 Self {
406 origin:
407 Point3d {
408 x: 0.0,
409 y: 0.0,
410 z: 0.0,
411 units: UnitLen::Mm,
412 },
413 x_axis:
414 Point3d {
415 x: 0.0,
416 y: 1.0,
417 z: 0.0,
418 units: _,
419 },
420 y_axis:
421 Point3d {
422 x: 0.0,
423 y: 0.0,
424 z: 1.0,
425 units: _,
426 },
427 z_axis: _,
428 } => return PlaneData::YZ,
429 Self {
430 origin:
431 Point3d {
432 x: 0.0,
433 y: 0.0,
434 z: 0.0,
435 units: UnitLen::Mm,
436 },
437 x_axis:
438 Point3d {
439 x: 0.0,
440 y: -1.0,
441 z: 0.0,
442 units: _,
443 },
444 y_axis:
445 Point3d {
446 x: 0.0,
447 y: 0.0,
448 z: 1.0,
449 units: _,
450 },
451 z_axis: _,
452 } => return PlaneData::NegYZ,
453 _ => {}
454 }
455 }
456
457 PlaneData::Plane(Self {
458 origin: self.origin,
459 x_axis: self.x_axis,
460 y_axis: self.y_axis,
461 z_axis: self.z_axis,
462 })
463 }
464
465 pub(crate) fn is_right_handed(&self) -> bool {
466 let lhs = self
469 .x_axis
470 .axes_cross_product(&self.y_axis)
471 .axes_dot_product(&self.z_axis);
472 let rhs_x = self.x_axis.axes_dot_product(&self.x_axis);
473 let rhs_y = self.y_axis.axes_dot_product(&self.y_axis);
474 let rhs_z = self.z_axis.axes_dot_product(&self.z_axis);
475 let rhs = (rhs_x * rhs_y * rhs_z).sqrt();
476 (lhs - rhs).abs() <= 0.0001
478 }
479
480 #[cfg(test)]
481 pub(crate) fn is_left_handed(&self) -> bool {
482 !self.is_right_handed()
483 }
484
485 pub(crate) fn make_right_handed(self) -> Self {
486 if self.is_right_handed() {
487 return self;
488 }
489 Self {
491 origin: self.origin,
492 x_axis: self.x_axis.negated(),
493 y_axis: self.y_axis,
494 z_axis: self.z_axis,
495 }
496 }
497}
498
499impl TryFrom<PlaneData> for PlaneInfo {
500 type Error = KclError;
501
502 fn try_from(value: PlaneData) -> Result<Self, Self::Error> {
503 if let PlaneData::Plane(info) = value {
504 return Ok(info);
505 }
506 let name = match value {
507 PlaneData::XY => PlaneName::Xy,
508 PlaneData::NegXY => PlaneName::NegXy,
509 PlaneData::XZ => PlaneName::Xz,
510 PlaneData::NegXZ => PlaneName::NegXz,
511 PlaneData::YZ => PlaneName::Yz,
512 PlaneData::NegYZ => PlaneName::NegYz,
513 PlaneData::Plane(_) => {
514 return Err(KclError::new_internal(KclErrorDetails::new(
516 format!("PlaneData {value:?} not found"),
517 Default::default(),
518 )));
519 }
520 };
521
522 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
523 KclError::new_internal(KclErrorDetails::new(
524 format!("Plane {name} not found"),
525 Default::default(),
526 ))
527 })?;
528
529 Ok(info.clone())
530 }
531}
532
533impl From<PlaneData> for PlaneType {
534 fn from(value: PlaneData) -> Self {
535 match value {
536 PlaneData::XY => PlaneType::XY,
537 PlaneData::NegXY => PlaneType::XY,
538 PlaneData::XZ => PlaneType::XZ,
539 PlaneData::NegXZ => PlaneType::XZ,
540 PlaneData::YZ => PlaneType::YZ,
541 PlaneData::NegYZ => PlaneType::YZ,
542 PlaneData::Plane(_) => PlaneType::Custom,
543 }
544 }
545}
546
547impl Plane {
548 pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Result<Self, KclError> {
549 let id = exec_state.next_uuid();
550 Ok(Plane {
551 id,
552 artifact_id: id.into(),
553 info: PlaneInfo::try_from(value.clone())?,
554 value: value.into(),
555 meta: vec![],
556 })
557 }
558
559 pub fn is_standard(&self) -> bool {
561 !matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
562 }
563
564 pub fn project(&self, point: Point3d) -> Point3d {
567 let v = point - self.info.origin;
568 let dot = v.axes_dot_product(&self.info.z_axis);
569
570 point - self.info.z_axis * dot
571 }
572}
573
574#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
576#[ts(export)]
577#[serde(rename_all = "camelCase")]
578pub struct Face {
579 pub id: uuid::Uuid,
581 pub artifact_id: ArtifactId,
583 pub value: String,
585 pub x_axis: Point3d,
587 pub y_axis: Point3d,
589 pub solid: Box<Solid>,
591 pub units: UnitLen,
592 #[serde(skip)]
593 pub meta: Vec<Metadata>,
594}
595
596#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
598#[ts(export)]
599#[display(style = "camelCase")]
600pub enum PlaneType {
601 #[serde(rename = "XY", alias = "xy")]
602 #[display("XY")]
603 XY,
604 #[serde(rename = "XZ", alias = "xz")]
605 #[display("XZ")]
606 XZ,
607 #[serde(rename = "YZ", alias = "yz")]
608 #[display("YZ")]
609 YZ,
610 #[display("Custom")]
612 Custom,
613 #[display("Uninit")]
615 Uninit,
616}
617
618#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
619#[ts(export)]
620#[serde(tag = "type", rename_all = "camelCase")]
621pub struct Sketch {
622 pub id: uuid::Uuid,
624 pub paths: Vec<Path>,
628 #[serde(default, skip_serializing_if = "Vec::is_empty")]
630 pub inner_paths: Vec<Path>,
631 pub on: SketchSurface,
633 pub start: BasePath,
635 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
637 pub tags: IndexMap<String, TagIdentifier>,
638 pub artifact_id: ArtifactId,
641 #[ts(skip)]
642 pub original_id: uuid::Uuid,
643 #[serde(skip)]
645 pub mirror: Option<uuid::Uuid>,
646 pub units: UnitLen,
647 #[serde(skip)]
649 pub meta: Vec<Metadata>,
650 #[serde(default = "very_true", skip_serializing_if = "is_true")]
652 pub is_closed: bool,
653}
654
655fn is_true(b: &bool) -> bool {
656 *b
657}
658
659impl Sketch {
660 pub(crate) fn build_sketch_mode_cmds(
663 &self,
664 exec_state: &mut ExecState,
665 inner_cmd: ModelingCmdReq,
666 ) -> Vec<ModelingCmdReq> {
667 vec![
668 ModelingCmdReq {
671 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
672 animated: false,
673 ortho: false,
674 entity_id: self.on.id(),
675 adjust_camera: false,
676 planar_normal: if let SketchSurface::Plane(plane) = &self.on {
677 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
679 Some(normal.into())
680 } else {
681 None
682 },
683 }),
684 cmd_id: exec_state.next_uuid().into(),
685 },
686 inner_cmd,
687 ModelingCmdReq {
688 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
689 cmd_id: exec_state.next_uuid().into(),
690 },
691 ]
692 }
693}
694
695#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
697#[ts(export)]
698#[serde(tag = "type", rename_all = "camelCase")]
699pub enum SketchSurface {
700 Plane(Box<Plane>),
701 Face(Box<Face>),
702}
703
704impl SketchSurface {
705 pub(crate) fn id(&self) -> uuid::Uuid {
706 match self {
707 SketchSurface::Plane(plane) => plane.id,
708 SketchSurface::Face(face) => face.id,
709 }
710 }
711 pub(crate) fn x_axis(&self) -> Point3d {
712 match self {
713 SketchSurface::Plane(plane) => plane.info.x_axis,
714 SketchSurface::Face(face) => face.x_axis,
715 }
716 }
717 pub(crate) fn y_axis(&self) -> Point3d {
718 match self {
719 SketchSurface::Plane(plane) => plane.info.y_axis,
720 SketchSurface::Face(face) => face.y_axis,
721 }
722 }
723}
724
725#[derive(Debug, Clone)]
726pub(crate) enum GetTangentialInfoFromPathsResult {
727 PreviousPoint([f64; 2]),
728 Arc {
729 center: [f64; 2],
730 ccw: bool,
731 },
732 Circle {
733 center: [f64; 2],
734 ccw: bool,
735 radius: f64,
736 },
737 Ellipse {
738 center: [f64; 2],
739 ccw: bool,
740 major_axis: [f64; 2],
741 _minor_radius: f64,
742 },
743}
744
745impl GetTangentialInfoFromPathsResult {
746 pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
747 match self {
748 GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
749 GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
750 crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
751 }
752 GetTangentialInfoFromPathsResult::Circle {
755 center, radius, ccw, ..
756 } => [center[0] + radius, center[1] + if *ccw { -1.0 } else { 1.0 }],
757 GetTangentialInfoFromPathsResult::Ellipse {
758 center,
759 major_axis,
760 ccw,
761 ..
762 } => [center[0] + major_axis[0], center[1] + if *ccw { -1.0 } else { 1.0 }],
763 }
764 }
765}
766
767impl Sketch {
768 pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
769 let mut tag_identifier: TagIdentifier = tag.into();
770 let base = current_path.get_base();
771 tag_identifier.info.push((
772 exec_state.stack().current_epoch(),
773 TagEngineInfo {
774 id: base.geo_meta.id,
775 sketch: self.id,
776 path: Some(current_path.clone()),
777 surface: None,
778 },
779 ));
780
781 self.tags.insert(tag.name.to_string(), tag_identifier);
782 }
783
784 pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
785 for t in tags {
786 match self.tags.get_mut(&t.value) {
787 Some(id) => {
788 id.merge_info(t);
789 }
790 None => {
791 self.tags.insert(t.value.clone(), t.clone());
792 }
793 }
794 }
795 }
796
797 pub(crate) fn latest_path(&self) -> Option<&Path> {
799 self.paths.last()
800 }
801
802 pub(crate) fn current_pen_position(&self) -> Result<Point2d, KclError> {
806 let Some(path) = self.latest_path() else {
807 return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units));
808 };
809
810 let to = path.get_base().to;
811 Ok(Point2d::new(to[0], to[1], path.get_base().units))
812 }
813
814 pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
815 let Some(path) = self.latest_path() else {
816 return GetTangentialInfoFromPathsResult::PreviousPoint(self.start.to);
817 };
818 path.get_tangential_info()
819 }
820}
821
822#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
823#[ts(export)]
824#[serde(tag = "type", rename_all = "camelCase")]
825pub struct Solid {
826 pub id: uuid::Uuid,
828 pub artifact_id: ArtifactId,
830 pub value: Vec<ExtrudeSurface>,
832 pub sketch: Sketch,
834 pub height: f64,
836 pub start_cap_id: Option<uuid::Uuid>,
838 pub end_cap_id: Option<uuid::Uuid>,
840 #[serde(default, skip_serializing_if = "Vec::is_empty")]
842 pub edge_cuts: Vec<EdgeCut>,
843 pub units: UnitLen,
845 pub sectional: bool,
847 #[serde(skip)]
849 pub meta: Vec<Metadata>,
850}
851
852impl Solid {
853 pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
854 self.edge_cuts.iter().map(|foc| foc.id())
855 }
856
857 pub(crate) fn height_in_mm(&self) -> f64 {
858 self.units.adjust_to(self.height, UnitLen::Mm).0
859 }
860}
861
862#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
864#[ts(export)]
865#[serde(tag = "type", rename_all = "camelCase")]
866pub enum EdgeCut {
867 Fillet {
869 id: uuid::Uuid,
871 radius: TyF64,
872 #[serde(rename = "edgeId")]
874 edge_id: uuid::Uuid,
875 tag: Box<Option<TagNode>>,
876 },
877 Chamfer {
879 id: uuid::Uuid,
881 length: TyF64,
882 #[serde(rename = "edgeId")]
884 edge_id: uuid::Uuid,
885 tag: Box<Option<TagNode>>,
886 },
887}
888
889impl EdgeCut {
890 pub fn id(&self) -> uuid::Uuid {
891 match self {
892 EdgeCut::Fillet { id, .. } => *id,
893 EdgeCut::Chamfer { id, .. } => *id,
894 }
895 }
896
897 pub fn set_id(&mut self, id: uuid::Uuid) {
898 match self {
899 EdgeCut::Fillet { id: i, .. } => *i = id,
900 EdgeCut::Chamfer { id: i, .. } => *i = id,
901 }
902 }
903
904 pub fn edge_id(&self) -> uuid::Uuid {
905 match self {
906 EdgeCut::Fillet { edge_id, .. } => *edge_id,
907 EdgeCut::Chamfer { edge_id, .. } => *edge_id,
908 }
909 }
910
911 pub fn set_edge_id(&mut self, id: uuid::Uuid) {
912 match self {
913 EdgeCut::Fillet { edge_id: i, .. } => *i = id,
914 EdgeCut::Chamfer { edge_id: i, .. } => *i = id,
915 }
916 }
917
918 pub fn tag(&self) -> Option<TagNode> {
919 match self {
920 EdgeCut::Fillet { tag, .. } => *tag.clone(),
921 EdgeCut::Chamfer { tag, .. } => *tag.clone(),
922 }
923 }
924}
925
926#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS)]
927#[ts(export)]
928pub struct Point2d {
929 pub x: f64,
930 pub y: f64,
931 pub units: UnitLen,
932}
933
934impl Point2d {
935 pub const ZERO: Self = Self {
936 x: 0.0,
937 y: 0.0,
938 units: UnitLen::Mm,
939 };
940
941 pub fn new(x: f64, y: f64, units: UnitLen) -> Self {
942 Self { x, y, units }
943 }
944
945 pub fn into_x(self) -> TyF64 {
946 TyF64::new(self.x, self.units.into())
947 }
948
949 pub fn into_y(self) -> TyF64 {
950 TyF64::new(self.y, self.units.into())
951 }
952
953 pub fn ignore_units(self) -> [f64; 2] {
954 [self.x, self.y]
955 }
956}
957
958#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, Default)]
959#[ts(export)]
960pub struct Point3d {
961 pub x: f64,
962 pub y: f64,
963 pub z: f64,
964 pub units: UnitLen,
965}
966
967impl Point3d {
968 pub const ZERO: Self = Self {
969 x: 0.0,
970 y: 0.0,
971 z: 0.0,
972 units: UnitLen::Mm,
973 };
974
975 pub fn new(x: f64, y: f64, z: f64, units: UnitLen) -> Self {
976 Self { x, y, z, units }
977 }
978
979 pub const fn is_zero(&self) -> bool {
980 self.x == 0.0 && self.y == 0.0 && self.z == 0.0
981 }
982
983 pub fn axes_cross_product(&self, other: &Self) -> Self {
988 Self {
989 x: self.y * other.z - self.z * other.y,
990 y: self.z * other.x - self.x * other.z,
991 z: self.x * other.y - self.y * other.x,
992 units: UnitLen::Unknown,
993 }
994 }
995
996 pub fn axes_dot_product(&self, other: &Self) -> f64 {
1001 let x = self.x * other.x;
1002 let y = self.y * other.y;
1003 let z = self.z * other.z;
1004 x + y + z
1005 }
1006
1007 pub fn normalize(&self) -> Self {
1008 let len = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
1009 Point3d {
1010 x: self.x / len,
1011 y: self.y / len,
1012 z: self.z / len,
1013 units: UnitLen::Unknown,
1014 }
1015 }
1016
1017 pub fn as_3_dims(&self) -> ([f64; 3], UnitLen) {
1018 let p = [self.x, self.y, self.z];
1019 let u = self.units;
1020 (p, u)
1021 }
1022
1023 pub(crate) fn negated(self) -> Self {
1024 Self {
1025 x: -self.x,
1026 y: -self.y,
1027 z: -self.z,
1028 units: self.units,
1029 }
1030 }
1031}
1032
1033impl From<[TyF64; 3]> for Point3d {
1034 fn from(p: [TyF64; 3]) -> Self {
1035 Self {
1036 x: p[0].n,
1037 y: p[1].n,
1038 z: p[2].n,
1039 units: p[0].ty.expect_length(),
1040 }
1041 }
1042}
1043
1044impl From<Point3d> for Point3D {
1045 fn from(p: Point3d) -> Self {
1046 Self { x: p.x, y: p.y, z: p.z }
1047 }
1048}
1049
1050impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
1051 fn from(p: Point3d) -> Self {
1052 Self {
1053 x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0),
1054 y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0),
1055 z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0),
1056 }
1057 }
1058}
1059
1060impl Add for Point3d {
1061 type Output = Point3d;
1062
1063 fn add(self, rhs: Self) -> Self::Output {
1064 Point3d {
1066 x: self.x + rhs.x,
1067 y: self.y + rhs.y,
1068 z: self.z + rhs.z,
1069 units: self.units,
1070 }
1071 }
1072}
1073
1074impl AddAssign for Point3d {
1075 fn add_assign(&mut self, rhs: Self) {
1076 *self = *self + rhs
1077 }
1078}
1079
1080impl Sub for Point3d {
1081 type Output = Point3d;
1082
1083 fn sub(self, rhs: Self) -> Self::Output {
1084 let (x, y, z) = if rhs.units != self.units {
1085 (
1086 rhs.units.adjust_to(rhs.x, self.units).0,
1087 rhs.units.adjust_to(rhs.y, self.units).0,
1088 rhs.units.adjust_to(rhs.z, self.units).0,
1089 )
1090 } else {
1091 (rhs.x, rhs.y, rhs.z)
1092 };
1093 Point3d {
1094 x: self.x - x,
1095 y: self.y - y,
1096 z: self.z - z,
1097 units: self.units,
1098 }
1099 }
1100}
1101
1102impl SubAssign for Point3d {
1103 fn sub_assign(&mut self, rhs: Self) {
1104 *self = *self - rhs
1105 }
1106}
1107
1108impl Mul<f64> for Point3d {
1109 type Output = Point3d;
1110
1111 fn mul(self, rhs: f64) -> Self::Output {
1112 Point3d {
1113 x: self.x * rhs,
1114 y: self.y * rhs,
1115 z: self.z * rhs,
1116 units: self.units,
1117 }
1118 }
1119}
1120
1121#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1123#[ts(export)]
1124#[serde(rename_all = "camelCase")]
1125pub struct BasePath {
1126 #[ts(type = "[number, number]")]
1128 pub from: [f64; 2],
1129 #[ts(type = "[number, number]")]
1131 pub to: [f64; 2],
1132 pub units: UnitLen,
1133 pub tag: Option<TagNode>,
1135 #[serde(rename = "__geoMeta")]
1137 pub geo_meta: GeoMeta,
1138}
1139
1140impl BasePath {
1141 pub fn get_to(&self) -> [TyF64; 2] {
1142 let ty: NumericType = self.units.into();
1143 [TyF64::new(self.to[0], ty), TyF64::new(self.to[1], ty)]
1144 }
1145
1146 pub fn get_from(&self) -> [TyF64; 2] {
1147 let ty: NumericType = self.units.into();
1148 [TyF64::new(self.from[0], ty), TyF64::new(self.from[1], ty)]
1149 }
1150}
1151
1152#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1154#[ts(export)]
1155#[serde(rename_all = "camelCase")]
1156pub struct GeoMeta {
1157 pub id: uuid::Uuid,
1159 #[serde(flatten)]
1161 pub metadata: Metadata,
1162}
1163
1164#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1166#[ts(export)]
1167#[serde(tag = "type")]
1168pub enum Path {
1169 ToPoint {
1171 #[serde(flatten)]
1172 base: BasePath,
1173 },
1174 TangentialArcTo {
1176 #[serde(flatten)]
1177 base: BasePath,
1178 #[ts(type = "[number, number]")]
1180 center: [f64; 2],
1181 ccw: bool,
1183 },
1184 TangentialArc {
1186 #[serde(flatten)]
1187 base: BasePath,
1188 #[ts(type = "[number, number]")]
1190 center: [f64; 2],
1191 ccw: bool,
1193 },
1194 Circle {
1197 #[serde(flatten)]
1198 base: BasePath,
1199 #[ts(type = "[number, number]")]
1201 center: [f64; 2],
1202 radius: f64,
1204 ccw: bool,
1207 },
1208 CircleThreePoint {
1209 #[serde(flatten)]
1210 base: BasePath,
1211 #[ts(type = "[number, number]")]
1213 p1: [f64; 2],
1214 #[ts(type = "[number, number]")]
1216 p2: [f64; 2],
1217 #[ts(type = "[number, number]")]
1219 p3: [f64; 2],
1220 },
1221 ArcThreePoint {
1222 #[serde(flatten)]
1223 base: BasePath,
1224 #[ts(type = "[number, number]")]
1226 p1: [f64; 2],
1227 #[ts(type = "[number, number]")]
1229 p2: [f64; 2],
1230 #[ts(type = "[number, number]")]
1232 p3: [f64; 2],
1233 },
1234 Horizontal {
1236 #[serde(flatten)]
1237 base: BasePath,
1238 x: f64,
1240 },
1241 AngledLineTo {
1243 #[serde(flatten)]
1244 base: BasePath,
1245 x: Option<f64>,
1247 y: Option<f64>,
1249 },
1250 Base {
1252 #[serde(flatten)]
1253 base: BasePath,
1254 },
1255 Arc {
1257 #[serde(flatten)]
1258 base: BasePath,
1259 center: [f64; 2],
1261 radius: f64,
1263 ccw: bool,
1265 },
1266 Ellipse {
1267 #[serde(flatten)]
1268 base: BasePath,
1269 center: [f64; 2],
1270 major_axis: [f64; 2],
1271 minor_radius: f64,
1272 ccw: bool,
1273 },
1274 Conic {
1276 #[serde(flatten)]
1277 base: BasePath,
1278 },
1279}
1280
1281impl Path {
1282 pub fn get_id(&self) -> uuid::Uuid {
1283 match self {
1284 Path::ToPoint { base } => base.geo_meta.id,
1285 Path::Horizontal { base, .. } => base.geo_meta.id,
1286 Path::AngledLineTo { base, .. } => base.geo_meta.id,
1287 Path::Base { base } => base.geo_meta.id,
1288 Path::TangentialArcTo { base, .. } => base.geo_meta.id,
1289 Path::TangentialArc { base, .. } => base.geo_meta.id,
1290 Path::Circle { base, .. } => base.geo_meta.id,
1291 Path::CircleThreePoint { base, .. } => base.geo_meta.id,
1292 Path::Arc { base, .. } => base.geo_meta.id,
1293 Path::ArcThreePoint { base, .. } => base.geo_meta.id,
1294 Path::Ellipse { base, .. } => base.geo_meta.id,
1295 Path::Conic { base, .. } => base.geo_meta.id,
1296 }
1297 }
1298
1299 pub fn set_id(&mut self, id: uuid::Uuid) {
1300 match self {
1301 Path::ToPoint { base } => base.geo_meta.id = id,
1302 Path::Horizontal { base, .. } => base.geo_meta.id = id,
1303 Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
1304 Path::Base { base } => base.geo_meta.id = id,
1305 Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
1306 Path::TangentialArc { base, .. } => base.geo_meta.id = id,
1307 Path::Circle { base, .. } => base.geo_meta.id = id,
1308 Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
1309 Path::Arc { base, .. } => base.geo_meta.id = id,
1310 Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
1311 Path::Ellipse { base, .. } => base.geo_meta.id = id,
1312 Path::Conic { base, .. } => base.geo_meta.id = id,
1313 }
1314 }
1315
1316 pub fn get_tag(&self) -> Option<TagNode> {
1317 match self {
1318 Path::ToPoint { base } => base.tag.clone(),
1319 Path::Horizontal { base, .. } => base.tag.clone(),
1320 Path::AngledLineTo { base, .. } => base.tag.clone(),
1321 Path::Base { base } => base.tag.clone(),
1322 Path::TangentialArcTo { base, .. } => base.tag.clone(),
1323 Path::TangentialArc { base, .. } => base.tag.clone(),
1324 Path::Circle { base, .. } => base.tag.clone(),
1325 Path::CircleThreePoint { base, .. } => base.tag.clone(),
1326 Path::Arc { base, .. } => base.tag.clone(),
1327 Path::ArcThreePoint { base, .. } => base.tag.clone(),
1328 Path::Ellipse { base, .. } => base.tag.clone(),
1329 Path::Conic { base, .. } => base.tag.clone(),
1330 }
1331 }
1332
1333 pub fn get_base(&self) -> &BasePath {
1334 match self {
1335 Path::ToPoint { base } => base,
1336 Path::Horizontal { base, .. } => base,
1337 Path::AngledLineTo { base, .. } => base,
1338 Path::Base { base } => base,
1339 Path::TangentialArcTo { base, .. } => base,
1340 Path::TangentialArc { base, .. } => base,
1341 Path::Circle { base, .. } => base,
1342 Path::CircleThreePoint { base, .. } => base,
1343 Path::Arc { base, .. } => base,
1344 Path::ArcThreePoint { base, .. } => base,
1345 Path::Ellipse { base, .. } => base,
1346 Path::Conic { base, .. } => base,
1347 }
1348 }
1349
1350 pub fn get_from(&self) -> [TyF64; 2] {
1352 let p = &self.get_base().from;
1353 let ty: NumericType = self.get_base().units.into();
1354 [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1355 }
1356
1357 pub fn get_to(&self) -> [TyF64; 2] {
1359 let p = &self.get_base().to;
1360 let ty: NumericType = self.get_base().units.into();
1361 [TyF64::new(p[0], ty), TyF64::new(p[1], ty)]
1362 }
1363
1364 pub fn start_point_components(&self) -> ([f64; 2], NumericType) {
1366 let p = &self.get_base().from;
1367 let ty: NumericType = self.get_base().units.into();
1368 (*p, ty)
1369 }
1370
1371 pub fn end_point_components(&self) -> ([f64; 2], NumericType) {
1373 let p = &self.get_base().to;
1374 let ty: NumericType = self.get_base().units.into();
1375 (*p, ty)
1376 }
1377
1378 pub fn length(&self) -> Option<TyF64> {
1381 let n = match self {
1382 Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
1383 Some(linear_distance(&self.get_base().from, &self.get_base().to))
1384 }
1385 Self::TangentialArc {
1386 base: _,
1387 center,
1388 ccw: _,
1389 }
1390 | Self::TangentialArcTo {
1391 base: _,
1392 center,
1393 ccw: _,
1394 } => {
1395 let radius = linear_distance(&self.get_base().from, center);
1398 debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
1399 Some(linear_distance(&self.get_base().from, &self.get_base().to))
1401 }
1402 Self::Circle { radius, .. } => Some(2.0 * std::f64::consts::PI * radius),
1403 Self::CircleThreePoint { .. } => {
1404 let circle_center = crate::std::utils::calculate_circle_from_3_points([
1405 self.get_base().from,
1406 self.get_base().to,
1407 self.get_base().to,
1408 ]);
1409 let radius = linear_distance(
1410 &[circle_center.center[0], circle_center.center[1]],
1411 &self.get_base().from,
1412 );
1413 Some(2.0 * std::f64::consts::PI * radius)
1414 }
1415 Self::Arc { .. } => {
1416 Some(linear_distance(&self.get_base().from, &self.get_base().to))
1418 }
1419 Self::ArcThreePoint { .. } => {
1420 Some(linear_distance(&self.get_base().from, &self.get_base().to))
1422 }
1423 Self::Ellipse { .. } => {
1424 None
1426 }
1427 Self::Conic { .. } => {
1428 None
1430 }
1431 };
1432 n.map(|n| TyF64::new(n, self.get_base().units.into()))
1433 }
1434
1435 pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
1436 match self {
1437 Path::ToPoint { base } => Some(base),
1438 Path::Horizontal { base, .. } => Some(base),
1439 Path::AngledLineTo { base, .. } => Some(base),
1440 Path::Base { base } => Some(base),
1441 Path::TangentialArcTo { base, .. } => Some(base),
1442 Path::TangentialArc { base, .. } => Some(base),
1443 Path::Circle { base, .. } => Some(base),
1444 Path::CircleThreePoint { base, .. } => Some(base),
1445 Path::Arc { base, .. } => Some(base),
1446 Path::ArcThreePoint { base, .. } => Some(base),
1447 Path::Ellipse { base, .. } => Some(base),
1448 Path::Conic { base, .. } => Some(base),
1449 }
1450 }
1451
1452 pub(crate) fn get_tangential_info(&self) -> GetTangentialInfoFromPathsResult {
1453 match self {
1454 Path::TangentialArc { center, ccw, .. }
1455 | Path::TangentialArcTo { center, ccw, .. }
1456 | Path::Arc { center, ccw, .. } => GetTangentialInfoFromPathsResult::Arc {
1457 center: *center,
1458 ccw: *ccw,
1459 },
1460 Path::ArcThreePoint { p1, p2, p3, .. } => {
1461 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1462 GetTangentialInfoFromPathsResult::Arc {
1463 center: circle.center,
1464 ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
1465 }
1466 }
1467 Path::Circle {
1468 center, ccw, radius, ..
1469 } => GetTangentialInfoFromPathsResult::Circle {
1470 center: *center,
1471 ccw: *ccw,
1472 radius: *radius,
1473 },
1474 Path::CircleThreePoint { p1, p2, p3, .. } => {
1475 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
1476 let center_point = [circle.center[0], circle.center[1]];
1477 GetTangentialInfoFromPathsResult::Circle {
1478 center: center_point,
1479 ccw: true,
1481 radius: circle.radius,
1482 }
1483 }
1484 Path::Ellipse {
1486 center,
1487 major_axis,
1488 minor_radius,
1489 ccw,
1490 ..
1491 } => GetTangentialInfoFromPathsResult::Ellipse {
1492 center: *center,
1493 major_axis: *major_axis,
1494 _minor_radius: *minor_radius,
1495 ccw: *ccw,
1496 },
1497 Path::Conic { .. }
1498 | Path::ToPoint { .. }
1499 | Path::Horizontal { .. }
1500 | Path::AngledLineTo { .. }
1501 | Path::Base { .. } => {
1502 let base = self.get_base();
1503 GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
1504 }
1505 }
1506 }
1507
1508 pub(crate) fn is_straight_line(&self) -> bool {
1510 matches!(self, Path::AngledLineTo { .. } | Path::ToPoint { .. })
1511 }
1512}
1513
1514#[rustfmt::skip]
1516fn linear_distance(
1517 [x0, y0]: &[f64; 2],
1518 [x1, y1]: &[f64; 2]
1519) -> f64 {
1520 let y_sq = (y1 - y0).powi(2);
1521 let x_sq = (x1 - x0).powi(2);
1522 (y_sq + x_sq).sqrt()
1523}
1524
1525#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1527#[ts(export)]
1528#[serde(tag = "type", rename_all = "camelCase")]
1529pub enum ExtrudeSurface {
1530 ExtrudePlane(ExtrudePlane),
1532 ExtrudeArc(ExtrudeArc),
1533 Chamfer(ChamferSurface),
1534 Fillet(FilletSurface),
1535}
1536
1537#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1539#[ts(export)]
1540#[serde(rename_all = "camelCase")]
1541pub struct ChamferSurface {
1542 pub face_id: uuid::Uuid,
1544 pub tag: Option<Node<TagDeclarator>>,
1546 #[serde(flatten)]
1548 pub geo_meta: GeoMeta,
1549}
1550
1551#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1553#[ts(export)]
1554#[serde(rename_all = "camelCase")]
1555pub struct FilletSurface {
1556 pub face_id: uuid::Uuid,
1558 pub tag: Option<Node<TagDeclarator>>,
1560 #[serde(flatten)]
1562 pub geo_meta: GeoMeta,
1563}
1564
1565#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1567#[ts(export)]
1568#[serde(rename_all = "camelCase")]
1569pub struct ExtrudePlane {
1570 pub face_id: uuid::Uuid,
1572 pub tag: Option<Node<TagDeclarator>>,
1574 #[serde(flatten)]
1576 pub geo_meta: GeoMeta,
1577}
1578
1579#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1581#[ts(export)]
1582#[serde(rename_all = "camelCase")]
1583pub struct ExtrudeArc {
1584 pub face_id: uuid::Uuid,
1586 pub tag: Option<Node<TagDeclarator>>,
1588 #[serde(flatten)]
1590 pub geo_meta: GeoMeta,
1591}
1592
1593impl ExtrudeSurface {
1594 pub fn get_id(&self) -> uuid::Uuid {
1595 match self {
1596 ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
1597 ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
1598 ExtrudeSurface::Fillet(f) => f.geo_meta.id,
1599 ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
1600 }
1601 }
1602
1603 pub fn get_tag(&self) -> Option<Node<TagDeclarator>> {
1604 match self {
1605 ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
1606 ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
1607 ExtrudeSurface::Fillet(f) => f.tag.clone(),
1608 ExtrudeSurface::Chamfer(c) => c.tag.clone(),
1609 }
1610 }
1611}