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::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::axis_or_reference::Axis2dOrEdgeReference;
25use crate::std::axis_or_reference::MirrorAcross3d;
26
27/// Mirror a solid.
28pub async fn mirror_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
29    let bodies = args.get_unlabeled_kw_arg("bodies", &RuntimeType::solids(), exec_state)?;
30    let across = args.get_kw_arg(
31        "across",
32        &RuntimeType::Union(vec![
33            RuntimeType::Primitive(PrimitiveType::Edge),
34            RuntimeType::Primitive(PrimitiveType::Axis3d),
35            RuntimeType::Primitive(PrimitiveType::Plane),
36            RuntimeType::segment(),
37        ]),
38        exec_state,
39    )?;
40
41    let bodies = inner_mirror_3d(bodies, across, exec_state, args).await?;
42    Ok(bodies.into())
43}
44
45/// Mirror a solid.
46async fn inner_mirror_3d(
47    bodies: Vec<Solid>,
48    across: MirrorAcross3d,
49    exec_state: &mut ExecState,
50    args: Args,
51) -> Result<Vec<Solid>, KclError> {
52    let mut mirrored_bodies = bodies.clone();
53
54    if args.ctx.no_engine_commands().await {
55        return Ok(mirrored_bodies);
56    }
57
58    exec_state
59        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &bodies)
60        .await?;
61
62    let across = match across {
63        MirrorAcross3d::Axis { direction, origin } => MirrorAcross::Axis {
64            axis: Point3d {
65                x: direction[0].to_mm(),
66                y: direction[1].to_mm(),
67                z: direction[2].to_mm(),
68            },
69            point: Point3d {
70                x: LengthUnit(origin[0].to_mm()),
71                y: LengthUnit(origin[1].to_mm()),
72                z: LengthUnit(origin[2].to_mm()),
73            },
74        },
75        MirrorAcross3d::Edge(edge) => MirrorAcross::Edge {
76            id: edge.get_engine_id(exec_state, &args)?,
77        },
78        MirrorAcross3d::Plane(mut plane) => {
79            if plane.is_uninitialized() {
80                crate::std::sketch::ensure_sketch_plane_in_engine(
81                    &mut plane,
82                    exec_state,
83                    &args.ctx,
84                    args.source_range,
85                    args.node_path.clone(),
86                )
87                .await?;
88            }
89            MirrorAcross::Plane { id: plane.id }
90        }
91    };
92
93    let resp = exec_state
94        .send_modeling_cmd(
95            ModelingCmdMeta::from_args(exec_state, &args),
96            ModelingCmd::from(
97                mcmd::EntityMirrorAcross::builder()
98                    .ids(bodies.iter().map(|body| body.id).collect())
99                    .across(across)
100                    .build(),
101            ),
102        )
103        .await?;
104
105    let OkWebSocketResponseData::Modeling {
106        modeling_response: OkModelingCmdResponse::EntityMirrorAcross(mirror_info),
107    } = &resp
108    else {
109        return Err(KclError::new_engine(KclErrorDetails::new(
110            format!("EntityMirrorAcross response was not as expected: {resp:?}"),
111            vec![args.source_range],
112        )));
113    };
114
115    if mirrored_bodies.len() != mirror_info.entity_face_edge_ids.len() {
116        return Err(KclError::new_engine(KclErrorDetails::new(
117            format!(
118                "EntityMirrorAcross response had {} mirrored bodies for {} input bodies",
119                mirror_info.entity_face_edge_ids.len(),
120                mirrored_bodies.len()
121            ),
122            vec![args.source_range],
123        )));
124    }
125
126    for (body, info) in mirrored_bodies.iter_mut().zip(mirror_info.entity_face_edge_ids.iter()) {
127        body.id = info.object_id;
128        body.artifact_id = ArtifactId::new(info.object_id);
129        if let Some(sketch) = body.sketch_mut() {
130            sketch.id = info.object_id;
131        }
132    }
133
134    Ok(mirrored_bodies)
135}
136
137/// Mirror a sketch.
138pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
139    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
140    let axis = args.get_kw_arg(
141        "axis",
142        &RuntimeType::Union(vec![
143            RuntimeType::Primitive(PrimitiveType::Edge),
144            RuntimeType::Primitive(PrimitiveType::Axis2d),
145            RuntimeType::segment(),
146        ]),
147        exec_state,
148    )?;
149
150    let sketches = inner_mirror_2d(sketches, axis, exec_state, args).await?;
151    Ok(sketches.into())
152}
153
154/// Mirror a sketch.
155async fn inner_mirror_2d(
156    sketches: Vec<Sketch>,
157    axis: Axis2dOrEdgeReference,
158    exec_state: &mut ExecState,
159    args: Args,
160) -> Result<Vec<Sketch>, KclError> {
161    let mut starting_sketches = sketches.clone();
162
163    if args.ctx.no_engine_commands().await {
164        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
165        // Track that information.
166        for sketch in starting_sketches.iter_mut() {
167            sketch.is_closed = crate::execution::ProfileClosed::Maybe;
168        }
169        return Ok(starting_sketches);
170    }
171
172    match axis {
173        Axis2dOrEdgeReference::Axis { direction, origin } => {
174            let resp = exec_state
175                .send_modeling_cmd(
176                    ModelingCmdMeta::from_args(exec_state, &args),
177                    ModelingCmd::from(
178                        mcmd::EntityMirror::builder()
179                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
180                            .axis(Point3d {
181                                x: direction[0].to_mm(),
182                                y: direction[1].to_mm(),
183                                z: 0.0,
184                            })
185                            .point(Point3d {
186                                x: LengthUnit(origin[0].to_mm()),
187                                y: LengthUnit(origin[1].to_mm()),
188                                z: LengthUnit(0.0),
189                            })
190                            .build(),
191                    ),
192                )
193                .await?;
194
195            if let OkWebSocketResponseData::Modeling {
196                modeling_response: OkModelingCmdResponse::EntityMirror(mirror_info),
197            } = &resp
198            {
199                let face_edge_info = &mirror_info.entity_face_edge_ids;
200
201                starting_sketches
202                    .iter_mut()
203                    .zip(face_edge_info.iter())
204                    .try_for_each(|(sketch, info)| {
205                        sketch.id = info.object_id;
206                        let first_edge = info.edges.first().copied();
207                        match first_edge {
208                            Some(edge) => sketch.mirror = Some(edge),
209                            None => {
210                                return Err(KclError::new_engine(KclErrorDetails::new(
211                                    "No edges found in mirror info".to_string(),
212                                    vec![args.source_range],
213                                )));
214                            }
215                        }
216                        // Currently, frontend doesn't know if mirror2d will close the sketch or not.
217                        // Track that information.
218                        sketch.is_closed = crate::execution::ProfileClosed::Maybe;
219                        Ok(())
220                    })?;
221            } else {
222                return Err(KclError::new_engine(KclErrorDetails::new(
223                    format!("EntityMirror response was not as expected: {resp:?}"),
224                    vec![args.source_range],
225                )));
226            };
227        }
228        Axis2dOrEdgeReference::Edge(edge) => {
229            let edge_id = edge.get_engine_id(exec_state, &args)?;
230
231            let resp = exec_state
232                .send_modeling_cmd(
233                    ModelingCmdMeta::from_args(exec_state, &args),
234                    ModelingCmd::from(
235                        mcmd::EntityMirrorAcrossEdge::builder()
236                            .ids(starting_sketches.iter().map(|sketch| sketch.id).collect())
237                            .edge_id(edge_id)
238                            .build(),
239                    ),
240                )
241                .await?;
242
243            if let OkWebSocketResponseData::Modeling {
244                modeling_response: OkModelingCmdResponse::EntityMirrorAcrossEdge(mirror_info),
245            } = &resp
246            {
247                let face_edge_info = &mirror_info.entity_face_edge_ids;
248
249                starting_sketches
250                    .iter_mut()
251                    .zip(face_edge_info.iter())
252                    .try_for_each(|(sketch, info)| {
253                        sketch.id = info.object_id;
254                        let first_edge = info.edges.first().copied();
255                        match first_edge {
256                            Some(edge) => sketch.mirror = Some(edge),
257                            None => {
258                                return Err(KclError::new_engine(KclErrorDetails::new(
259                                    "No edges found in mirror info".to_string(),
260                                    vec![args.source_range],
261                                )));
262                            }
263                        }
264                        Ok(())
265                    })?;
266            } else {
267                return Err(KclError::new_engine(KclErrorDetails::new(
268                    format!("EntityMirrorAcrossEdge response was not as expected: {resp:?}"),
269                    vec![args.source_range],
270                )));
271            };
272        }
273    }
274
275    Ok(starting_sketches)
276}