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].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
182pub 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
199async 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 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 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 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 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}