Skip to main content

kcl_lib/std/
mirror.rs

1//! Standard library mirror.
2
3use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kittycad_modeling_cmds::length_unit::LengthUnit;
7use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
8use kittycad_modeling_cmds::shared::MirrorAcross;
9use kittycad_modeling_cmds::shared::Point3d;
10use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
11use kittycad_modeling_cmds::{self as kcmc};
12
13use crate::errors::KclError;
14use crate::errors::KclErrorDetails;
15use crate::execution::ArtifactId;
16use crate::execution::ExecState;
17use crate::execution::GeometryWithImportedGeometry;
18use crate::execution::KclValue;
19use crate::execution::ModelingCmdMeta;
20use crate::execution::Sketch;
21use crate::execution::Solid;
22use crate::execution::types::PrimitiveType;
23use crate::execution::types::RuntimeType;
24use crate::std::Args;
25use crate::std::axis_or_reference::Axis2dOrEdgeReference;
26use crate::std::axis_or_reference::MirrorAcross3d;
27use crate::std::clone::fix_tags_and_references;
28
29/// Mirror a solid.
30pub async fn mirror_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31    let bodies = args.get_unlabeled_kw_arg("bodies", &RuntimeType::solids(), exec_state)?;
32    let across = args.get_kw_arg(
33        "across",
34        &RuntimeType::Union(vec![
35            RuntimeType::Primitive(PrimitiveType::Edge),
36            RuntimeType::Primitive(PrimitiveType::Axis3d),
37            RuntimeType::Primitive(PrimitiveType::Plane),
38            RuntimeType::segment(),
39        ]),
40        exec_state,
41    )?;
42
43    let bodies = inner_mirror_3d(bodies, across, exec_state, args).await?;
44    Ok(bodies.into())
45}
46
47/// Mirror a solid.
48async fn inner_mirror_3d(
49    bodies: Vec<Solid>,
50    across: MirrorAcross3d,
51    exec_state: &mut ExecState,
52    args: Args,
53) -> Result<Vec<Solid>, KclError> {
54    let unmapped_mirrored_bodies = bodies.clone();
55
56    if args.ctx.no_engine_commands().await {
57        return Ok(unmapped_mirrored_bodies);
58    }
59
60    exec_state
61        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &bodies)
62        .await?;
63
64    let across = match across {
65        MirrorAcross3d::Axis { direction, origin } => MirrorAcross::Axis {
66            axis: Point3d {
67                x: direction[0].to_mm(),
68                y: direction[1].to_mm(),
69                z: direction[2].to_mm(),
70            },
71            point: Point3d {
72                x: LengthUnit(origin[0].to_mm()),
73                y: LengthUnit(origin[1].to_mm()),
74                z: LengthUnit(origin[2].to_mm()),
75            },
76        },
77        MirrorAcross3d::Edge(edge) => MirrorAcross::Edge {
78            id: edge.get_engine_id(exec_state, &args)?,
79        },
80        MirrorAcross3d::Plane(mut plane) => {
81            if plane.is_uninitialized() {
82                crate::std::sketch::ensure_sketch_plane_in_engine(
83                    &mut plane,
84                    exec_state,
85                    &args.ctx,
86                    args.source_range,
87                    args.node_path.clone(),
88                )
89                .await?;
90            }
91            MirrorAcross::Plane { id: plane.id }
92        }
93    };
94
95    let old_body_ids = bodies.iter().map(|body| body.id).collect::<Vec<_>>();
96    let resp = exec_state
97        .send_modeling_cmd(
98            ModelingCmdMeta::from_args(exec_state, &args),
99            ModelingCmd::from(
100                mcmd::EntityMirrorAcross::builder()
101                    .ids(old_body_ids.clone())
102                    .across(across)
103                    .build(),
104            ),
105        )
106        .await?;
107
108    let OkWebSocketResponseData::Modeling {
109        modeling_response: OkModelingCmdResponse::EntityMirrorAcross(mirror_info),
110    } = &resp
111    else {
112        return Err(KclError::new_engine(KclErrorDetails::new(
113            format!("EntityMirrorAcross response was not as expected: {resp:?}"),
114            vec![args.source_range],
115        )));
116    };
117
118    if unmapped_mirrored_bodies.len() != mirror_info.entity_face_edge_ids.len() {
119        return Err(KclError::new_engine(KclErrorDetails::new(
120            format!(
121                "EntityMirrorAcross response had {} mirrored bodies for {} input bodies",
122                mirror_info.entity_face_edge_ids.len(),
123                unmapped_mirrored_bodies.len()
124            ),
125            vec![args.source_range],
126        )));
127    }
128
129    let mut mirrored_bodies = Vec::with_capacity(unmapped_mirrored_bodies.len());
130    for ((mut mirrored_body, old_id), info) in unmapped_mirrored_bodies
131        .into_iter()
132        .zip(old_body_ids)
133        .zip(mirror_info.entity_face_edge_ids.iter())
134    {
135        mirrored_body.id = info.object_id;
136        mirrored_body.artifact_id = ArtifactId::new(info.object_id);
137        let mut new_geometry = GeometryWithImportedGeometry::Solid(mirrored_body);
138        fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
139            .await
140            .map_err(|e| {
141                KclError::new_internal(KclErrorDetails::new(
142                    format!("failed to fix tags and references: {e:?}"),
143                    vec![args.source_range],
144                ))
145            })?;
146        let Some(mirrored_body) = new_geometry.into_solid() else {
147            let message = "failed to extract Solid from Geometry";
148            debug_assert!(false, "{message}");
149            return Err(KclError::new_internal(KclErrorDetails::new(
150                message.to_owned(),
151                vec![args.source_range],
152            )));
153        };
154        mirrored_bodies.push(mirrored_body);
155    }
156
157    Ok(mirrored_bodies)
158}
159
160/// Mirror a sketch.
161pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
162    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
163    let axis = args.get_kw_arg(
164        "axis",
165        &RuntimeType::Union(vec![
166            RuntimeType::Primitive(PrimitiveType::Edge),
167            RuntimeType::Primitive(PrimitiveType::Axis2d),
168            RuntimeType::segment(),
169        ]),
170        exec_state,
171    )?;
172
173    let sketches = inner_mirror_2d(sketches, axis, exec_state, args).await?;
174    Ok(sketches.into())
175}
176
177/// Mirror a sketch.
178async fn inner_mirror_2d(
179    sketches: Vec<Sketch>,
180    axis: Axis2dOrEdgeReference,
181    exec_state: &mut ExecState,
182    args: Args,
183) -> Result<Vec<Sketch>, KclError> {
184    let mut starting_sketches = sketches.clone();
185
186    if args.ctx.no_engine_commands().await {
187        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
188        // Track that information.
189        for sketch in starting_sketches.iter_mut() {
190            sketch.is_closed = crate::execution::ProfileClosed::Maybe;
191        }
192        return Ok(starting_sketches);
193    }
194
195    match axis {
196        Axis2dOrEdgeReference::Axis { direction, origin } => {
197            let resp = exec_state
198                .send_modeling_cmd(
199                    ModelingCmdMeta::from_args(exec_state, &args),
200                    ModelingCmd::from(
201                        mcmd::EntityMirror::builder()
202                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
203                            .axis(Point3d {
204                                x: direction[0].to_mm(),
205                                y: direction[1].to_mm(),
206                                z: 0.0,
207                            })
208                            .point(Point3d {
209                                x: LengthUnit(origin[0].to_mm()),
210                                y: LengthUnit(origin[1].to_mm()),
211                                z: LengthUnit(0.0),
212                            })
213                            .build(),
214                    ),
215                )
216                .await?;
217
218            if let OkWebSocketResponseData::Modeling {
219                modeling_response: OkModelingCmdResponse::EntityMirror(mirror_info),
220            } = &resp
221            {
222                let face_edge_info = &mirror_info.entity_face_edge_ids;
223
224                starting_sketches
225                    .iter_mut()
226                    .zip(face_edge_info.iter())
227                    .try_for_each(|(sketch, info)| {
228                        sketch.id = info.object_id;
229                        let first_edge = info.edges.first().copied();
230                        match first_edge {
231                            Some(edge) => sketch.mirror = Some(edge),
232                            None => {
233                                return Err(KclError::new_engine(KclErrorDetails::new(
234                                    "No edges found in mirror info".to_string(),
235                                    vec![args.source_range],
236                                )));
237                            }
238                        }
239                        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
240                        // Track that information.
241                        sketch.is_closed = crate::execution::ProfileClosed::Maybe;
242                        Ok(())
243                    })?;
244            } else {
245                return Err(KclError::new_engine(KclErrorDetails::new(
246                    format!("EntityMirror response was not as expected: {resp:?}"),
247                    vec![args.source_range],
248                )));
249            };
250        }
251        Axis2dOrEdgeReference::Edge(edge) => {
252            let edge_id = edge.get_engine_id(exec_state, &args)?;
253
254            let resp = exec_state
255                .send_modeling_cmd(
256                    ModelingCmdMeta::from_args(exec_state, &args),
257                    ModelingCmd::from(
258                        mcmd::EntityMirrorAcrossEdge::builder()
259                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
260                            .edge_id(edge_id)
261                            .build(),
262                    ),
263                )
264                .await?;
265
266            if let OkWebSocketResponseData::Modeling {
267                modeling_response: OkModelingCmdResponse::EntityMirrorAcrossEdge(mirror_info),
268            } = &resp
269            {
270                let face_edge_info = &mirror_info.entity_face_edge_ids;
271
272                starting_sketches
273                    .iter_mut()
274                    .zip(face_edge_info.iter())
275                    .try_for_each(|(sketch, info)| {
276                        sketch.id = info.object_id;
277                        let first_edge = info.edges.first().copied();
278                        match first_edge {
279                            Some(edge) => sketch.mirror = Some(edge),
280                            None => {
281                                return Err(KclError::new_engine(KclErrorDetails::new(
282                                    "No edges found in mirror info".to_string(),
283                                    vec![args.source_range],
284                                )));
285                            }
286                        }
287                        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
288                        // Track that information.
289                        sketch.is_closed = crate::execution::ProfileClosed::Maybe;
290                        Ok(())
291                    })?;
292            } else {
293                return Err(KclError::new_engine(KclErrorDetails::new(
294                    format!("EntityMirrorAcrossEdge response was not as expected: {resp:?}"),
295                    vec![args.source_range],
296                )));
297            };
298        }
299        // EdgeSpecifier variant exists for revolve, but mirror2d doesn't support edge specifiers.
300        Axis2dOrEdgeReference::EdgeSpecifier(_) => {
301            debug_assert!(false, "mirror2d does not support EdgeSpecifier, only Axis or Edge");
302            return Err(KclError::new_internal(KclErrorDetails::new(
303                "mirror2d does not support edge specifiers, only Axis or Edge".to_owned(),
304                vec![args.source_range],
305            )));
306        }
307    }
308
309    Ok(starting_sketches)
310}