1use 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
29pub 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
47async 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
160pub 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
177async 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 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 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 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 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}