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::ExecState;
16use crate::execution::GeometryWithImportedGeometry;
17use crate::execution::KclValue;
18use crate::execution::ModelingCmdMeta;
19use crate::execution::Sketch;
20use crate::execution::Solid;
21use crate::execution::types::PrimitiveType;
22use crate::execution::types::RuntimeType;
23use crate::std::Args;
24use crate::std::args::FromKclValue;
25use crate::std::axis_or_reference::Axis2dOrEdgeReference;
26use crate::std::axis_or_reference::MirrorAcross3d;
27use crate::std::clone::fix_tags_and_references;
28use crate::std::patterns::GeometryTrait;
29
30/// Mirror a solid.
31pub async fn mirror_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
32    let bodies = args.get_unlabeled_kw_arg("bodies", &RuntimeType::solids(), exec_state)?;
33    let across_value: KclValue = args.get_kw_arg("across", &RuntimeType::any(), exec_state)?;
34    let across = if crate::std::edge::is_edge_specifier_object(&across_value) {
35        MirrorAcross3d::EdgeSpecifier(crate::std::edge::parse_edge_specifier_value(&across_value, &args)?)
36    } else {
37        MirrorAcross3d::from_kcl_val(&across_value).ok_or_else(|| {
38            KclError::new_type(KclErrorDetails::new(
39                "across must be an Edge, Plane, Axis3d, Segment, or an edge specifier object".to_owned(),
40                vec![args.source_range],
41            ))
42        })?
43    };
44
45    let bodies = inner_mirror_3d(bodies, across, exec_state, args).await?;
46    Ok(bodies.into())
47}
48
49/// Mirror a solid.
50async fn inner_mirror_3d(
51    bodies: Vec<Solid>,
52    across: MirrorAcross3d,
53    exec_state: &mut ExecState,
54    args: Args,
55) -> Result<Vec<Solid>, KclError> {
56    let unmapped_mirrored_bodies = bodies.clone();
57
58    if args.ctx.no_engine_commands().await {
59        let mut unmapped_mirrored_bodies = unmapped_mirrored_bodies;
60        for mirrored_body in &mut unmapped_mirrored_bodies {
61            let id = exec_state.next_uuid();
62            mirrored_body.set_id(id);
63            mirrored_body.become_new_body(id, id.into());
64        }
65        return Ok(unmapped_mirrored_bodies);
66    }
67
68    exec_state
69        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &bodies)
70        .await?;
71
72    let across = match across {
73        MirrorAcross3d::Axis { direction, origin } => MirrorAcross::Axis {
74            axis: Point3d {
75                x: direction[0].unwrap_to_mm(),
76                y: direction[1].unwrap_to_mm(),
77                z: direction[2].unwrap_to_mm(),
78            },
79            point: Point3d {
80                x: LengthUnit(origin[0].unwrap_to_mm()),
81                y: LengthUnit(origin[1].unwrap_to_mm()),
82                z: LengthUnit(origin[2].unwrap_to_mm()),
83            },
84        },
85        MirrorAcross3d::Edge(edge) => {
86            let edge_id = edge.get_engine_id(exec_state, &args)?;
87            let source_range = args
88                .labeled
89                .get("across")
90                .map(|arg| arg.source_range)
91                .unwrap_or(args.source_range);
92            crate::std::edge::record_refactor_meta_for_consumed_edge(exec_state, edge_id, source_range, &args).await;
93            crate::std::edge::record_refactor_meta_for_direct_edge(exec_state, edge_id, source_range, &args).await?;
94            MirrorAcross::Edge { id: edge_id }
95        }
96        MirrorAcross3d::EdgeSpecifier(specifier) => MirrorAcross::EdgeReference {
97            reference: crate::std::edge::resolve_edge_specifier_with_adjacent_faces_or_tag_ids(
98                &specifier, exec_state, &args,
99            )
100            .await?,
101        },
102        MirrorAcross3d::Plane(mut plane) => {
103            if plane.is_uninitialized() {
104                crate::std::sketch::ensure_sketch_plane_in_engine(
105                    &mut plane,
106                    exec_state,
107                    &args.ctx,
108                    args.source_range,
109                    args.node_path.clone(),
110                )
111                .await?;
112            }
113            MirrorAcross::Plane { id: plane.id }
114        }
115    };
116
117    let old_body_ids = bodies.iter().map(|body| body.id).collect::<Vec<_>>();
118    let resp = exec_state
119        .send_modeling_cmd(
120            ModelingCmdMeta::from_args(exec_state, &args),
121            ModelingCmd::from(
122                mcmd::EntityMirrorAcross::builder()
123                    .ids(old_body_ids.clone())
124                    .across(across)
125                    .build(),
126            ),
127        )
128        .await?;
129
130    let OkWebSocketResponseData::Modeling {
131        modeling_response: OkModelingCmdResponse::EntityMirrorAcross(mirror_info),
132    } = &resp
133    else {
134        return Err(KclError::new_engine(KclErrorDetails::new(
135            format!("EntityMirrorAcross response was not as expected: {resp:?}"),
136            vec![args.source_range],
137        )));
138    };
139
140    if unmapped_mirrored_bodies.len() != mirror_info.entity_face_edge_ids.len() {
141        return Err(KclError::new_engine(KclErrorDetails::new(
142            format!(
143                "EntityMirrorAcross response had {} mirrored bodies for {} input bodies",
144                mirror_info.entity_face_edge_ids.len(),
145                unmapped_mirrored_bodies.len()
146            ),
147            vec![args.source_range],
148        )));
149    }
150
151    let mut mirrored_bodies = Vec::with_capacity(unmapped_mirrored_bodies.len());
152    for (mut mirrored_body, info) in unmapped_mirrored_bodies
153        .into_iter()
154        .zip(mirror_info.entity_face_edge_ids.iter())
155    {
156        let old_id = mirrored_body.id;
157        let source_topology_id = mirrored_body.topology_id();
158        mirrored_body.id = info.object_id;
159        mirrored_body.become_new_body(info.object_id, info.object_id.into());
160        let mut new_geometry = GeometryWithImportedGeometry::Solid(mirrored_body);
161        fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args)
162            .await
163            .map_err(|e| {
164                KclError::new_internal(KclErrorDetails::new(
165                    format!("failed to fix tags and references: {e:?}"),
166                    vec![args.source_range],
167                ))
168            })?;
169        let Some(mirrored_body) = new_geometry.into_solid() else {
170            let message = "failed to extract Solid from Geometry";
171            debug_assert!(false, "{message}");
172            return Err(KclError::new_internal(KclErrorDetails::new(
173                message.to_owned(),
174                vec![args.source_range],
175            )));
176        };
177        mirrored_bodies.push(mirrored_body);
178    }
179
180    Ok(mirrored_bodies)
181}
182
183/// Mirror a sketch.
184pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
185    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
186    let axis = args.get_kw_arg(
187        "axis",
188        &RuntimeType::Union(vec![
189            RuntimeType::Primitive(PrimitiveType::Edge),
190            RuntimeType::Primitive(PrimitiveType::Axis2d),
191            RuntimeType::segment(),
192        ]),
193        exec_state,
194    )?;
195
196    let sketches = inner_mirror_2d(sketches, axis, exec_state, args).await?;
197    Ok(sketches.into())
198}
199
200/// Mirror a sketch.
201async fn inner_mirror_2d(
202    sketches: Vec<Sketch>,
203    axis: Axis2dOrEdgeReference,
204    exec_state: &mut ExecState,
205    args: Args,
206) -> Result<Vec<Sketch>, KclError> {
207    let mut starting_sketches = sketches.clone();
208
209    if args.ctx.no_engine_commands().await {
210        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
211        // Track that information.
212        for sketch in starting_sketches.iter_mut() {
213            sketch.is_closed = crate::execution::ProfileClosed::Maybe;
214        }
215        return Ok(starting_sketches);
216    }
217
218    match axis {
219        Axis2dOrEdgeReference::Axis { direction, origin } => {
220            let resp = exec_state
221                .send_modeling_cmd(
222                    ModelingCmdMeta::from_args(exec_state, &args),
223                    ModelingCmd::from(
224                        mcmd::EntityMirror::builder()
225                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
226                            .axis(Point3d {
227                                x: direction[0].unwrap_to_mm(),
228                                y: direction[1].unwrap_to_mm(),
229                                z: 0.0,
230                            })
231                            .point(Point3d {
232                                x: LengthUnit(origin[0].unwrap_to_mm()),
233                                y: LengthUnit(origin[1].unwrap_to_mm()),
234                                z: LengthUnit(0.0),
235                            })
236                            .build(),
237                    ),
238                )
239                .await?;
240
241            if let OkWebSocketResponseData::Modeling {
242                modeling_response: OkModelingCmdResponse::EntityMirror(mirror_info),
243            } = &resp
244            {
245                let face_edge_info = &mirror_info.entity_face_edge_ids;
246
247                starting_sketches
248                    .iter_mut()
249                    .zip(face_edge_info.iter())
250                    .try_for_each(|(sketch, info)| {
251                        sketch.id = info.object_id;
252                        let first_edge = info.edges.first().copied();
253                        match first_edge {
254                            Some(edge) => sketch.mirror = Some(edge),
255                            None => {
256                                return Err(KclError::new_engine(KclErrorDetails::new(
257                                    "No edges found in mirror info".to_string(),
258                                    vec![args.source_range],
259                                )));
260                            }
261                        }
262                        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
263                        // Track that information.
264                        sketch.is_closed = crate::execution::ProfileClosed::Maybe;
265                        Ok(())
266                    })?;
267            } else {
268                return Err(KclError::new_engine(KclErrorDetails::new(
269                    format!("EntityMirror response was not as expected: {resp:?}"),
270                    vec![args.source_range],
271                )));
272            };
273        }
274        Axis2dOrEdgeReference::Edge(edge) => {
275            let edge_id = edge.get_engine_id(exec_state, &args)?;
276
277            let resp = exec_state
278                .send_modeling_cmd(
279                    ModelingCmdMeta::from_args(exec_state, &args),
280                    ModelingCmd::from(
281                        mcmd::EntityMirrorAcrossEdge::builder()
282                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
283                            .edge_id(edge_id)
284                            .build(),
285                    ),
286                )
287                .await?;
288
289            if let OkWebSocketResponseData::Modeling {
290                modeling_response: OkModelingCmdResponse::EntityMirrorAcrossEdge(mirror_info),
291            } = &resp
292            {
293                let face_edge_info = &mirror_info.entity_face_edge_ids;
294
295                starting_sketches
296                    .iter_mut()
297                    .zip(face_edge_info.iter())
298                    .try_for_each(|(sketch, info)| {
299                        sketch.id = info.object_id;
300                        let first_edge = info.edges.first().copied();
301                        match first_edge {
302                            Some(edge) => sketch.mirror = Some(edge),
303                            None => {
304                                return Err(KclError::new_engine(KclErrorDetails::new(
305                                    "No edges found in mirror info".to_string(),
306                                    vec![args.source_range],
307                                )));
308                            }
309                        }
310                        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
311                        // Track that information.
312                        sketch.is_closed = crate::execution::ProfileClosed::Maybe;
313                        Ok(())
314                    })?;
315            } else {
316                return Err(KclError::new_engine(KclErrorDetails::new(
317                    format!("EntityMirrorAcrossEdge response was not as expected: {resp:?}"),
318                    vec![args.source_range],
319                )));
320            };
321        }
322        // EdgeSpecifier variant exists for revolve, but mirror2d doesn't support edge specifiers.
323        Axis2dOrEdgeReference::EdgeSpecifier(_) => {
324            debug_assert!(false, "mirror2d does not support EdgeSpecifier, only Axis or Edge");
325            return Err(KclError::new_internal(KclErrorDetails::new(
326                "mirror2d does not support edge specifiers, only Axis or Edge".to_owned(),
327                vec![args.source_range],
328            )));
329        }
330    }
331
332    Ok(starting_sketches)
333}
334
335#[cfg(test)]
336mod tests {
337    use crate::execution::MockConfig;
338
339    #[tokio::test(flavor = "multi_thread")]
340    async fn mock_mirror_has_independent_consumption_identity() {
341        let code = r#"// Mirrored rectangular cut blocks
342@settings(defaultLengthUnit = mm, kclVersion = 2.0)
343
344baseStartX = 1mm
345baseWidth = 10mm
346baseHeight = 10mm
347partThickness = 5mm
348cutStartX = 3mm
349cutStartY = 3mm
350cutWidth = 2mm
351cutHeight = 2mm
352
353baseSketch = sketch(on = XY) {
354  baseBottom = line(start = [var 1mm, var 0mm], end = [var 11mm, var 0mm])
355  baseRight = line(start = [var 11mm, var 0mm], end = [var 11mm, var 10mm])
356  baseTop = line(start = [var 11mm, var 10mm], end = [var 1mm, var 10mm])
357  baseLeft = line(start = [var 1mm, var 10mm], end = [var 1mm, var 0mm])
358
359  coincident([baseBottom.end, baseRight.start])
360  coincident([baseRight.end, baseTop.start])
361  coincident([baseTop.end, baseLeft.start])
362  coincident([baseLeft.end, baseBottom.start])
363  horizontal(baseBottom)
364  horizontal(baseTop)
365  vertical(baseRight)
366  vertical(baseLeft)
367  horizontalDistance([ORIGIN, baseBottom.start]) == baseStartX
368  verticalDistance([ORIGIN, baseBottom.start]) == 0mm
369  horizontalDistance([baseBottom.start, baseBottom.end]) == baseWidth
370  verticalDistance([baseBottom.start, baseLeft.start]) == baseHeight
371}
372
373baseRegion = region(segments = [
374  baseSketch.baseBottom,
375  baseSketch.baseRight
376])
377base = extrude(baseRegion, length = partThickness)
378hiddenBaseSketch = hide(baseSketch)
379
380mirroredBase = mirror3d([base], across = YZ)
381
382cutSketch = sketch(on = XY) {
383  cutBottom = line(start = [var 3mm, var 3mm], end = [var 5mm, var 3mm])
384  cutRight = line(start = [var 5mm, var 3mm], end = [var 5mm, var 5mm])
385  cutTop = line(start = [var 5mm, var 5mm], end = [var 3mm, var 5mm])
386  cutLeft = line(start = [var 3mm, var 5mm], end = [var 3mm, var 3mm])
387
388  coincident([cutBottom.end, cutRight.start])
389  coincident([cutRight.end, cutTop.start])
390  coincident([cutTop.end, cutLeft.start])
391  coincident([cutLeft.end, cutBottom.start])
392  horizontal(cutBottom)
393  horizontal(cutTop)
394  vertical(cutRight)
395  vertical(cutLeft)
396  horizontalDistance([ORIGIN, cutBottom.start]) == cutStartX
397  verticalDistance([ORIGIN, cutBottom.start]) == cutStartY
398  horizontalDistance([cutBottom.start, cutBottom.end]) == cutWidth
399  verticalDistance([cutBottom.start, cutLeft.start]) == cutHeight
400}
401
402cutRegion = region(segments = [
403  cutSketch.cutBottom,
404  cutSketch.cutRight
405])
406tool = extrude(cutRegion, length = partThickness)
407hiddenCutSketch = hide(cutSketch)
408
409mirroredTool = mirror3d([tool], across = YZ)
410firstCut = subtract(base, tools = [tool])
411secondCut = subtract(mirroredBase, tools = [mirroredTool])
412"#;
413
414        let program = crate::Program::parse_no_errs(code).unwrap();
415        let ctx = crate::ExecutorContext::new_mock(None).await;
416        let result = ctx.run_mock(&program, &MockConfig::default()).await;
417        ctx.close().await;
418        result.unwrap();
419    }
420}