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::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
30pub 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
49async 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
183pub 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
200async 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 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 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 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 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}