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