1use anyhow::Result;
4use kcl_error::CompilationIssue;
5use kcmc::ModelingCmd;
6use kcmc::each_cmd as mcmd;
7use kcmc::length_unit::LengthUnit;
8use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
9use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
10use kittycad_modeling_cmds::{self as kcmc};
11
12use super::DEFAULT_TOLERANCE_MM;
13use super::args::TyF64;
14use super::solid_consumption::record_consumed_solids;
15use super::solid_consumption::validate_solids_not_consumed;
16use crate::errors::KclError;
17use crate::errors::KclErrorDetails;
18use crate::execution::ConsumedSolidOperation;
19use crate::execution::ExecState;
20use crate::execution::KclValue;
21use crate::execution::ModelingCmdMeta;
22use crate::execution::Solid;
23use crate::execution::annotations;
24use crate::execution::types::RuntimeType;
25use crate::std::Args;
26use crate::std::patterns::GeometryTrait;
27
28pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
30 let solids: Vec<Solid> =
31 args.get_unlabeled_kw_arg("solids", &RuntimeType::Union(vec![RuntimeType::solids()]), exec_state)?;
32 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
33 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
34 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
35
36 if solids.len() < 2 {
37 return Err(KclError::new_semantic(KclErrorDetails::new(
38 "At least two solids are required for a union operation.".to_string(),
39 vec![args.source_range],
40 )));
41 }
42
43 let solids = inner_union(solids, tolerance, csg_algorithm, exec_state, args).await?;
44 Ok(solids.into())
45}
46
47pub enum CsgAlgorithm {
48 Latest,
49 Legacy,
50}
51
52impl CsgAlgorithm {
53 pub fn legacy(is_legacy: bool) -> Self {
54 if is_legacy { Self::Legacy } else { Self::Latest }
55 }
56 pub fn is_legacy(&self) -> bool {
57 match self {
58 CsgAlgorithm::Latest => false,
59 CsgAlgorithm::Legacy => true,
60 }
61 }
62}
63
64pub(crate) async fn inner_union(
65 solids: Vec<Solid>,
66 tolerance: Option<TyF64>,
67 csg_algorithm: CsgAlgorithm,
68 exec_state: &mut ExecState,
69 args: Args,
70) -> Result<Vec<Solid>, KclError> {
71 validate_solids_not_consumed(&solids, exec_state, args.source_range)?;
72
73 let solid_out_id = exec_state.next_uuid();
74
75 let mut solid = solids[0].clone();
76 solid.set_id(solid_out_id);
77 solid.artifact_id = solid_out_id.into();
78 let mut new_solids = vec![solid.clone()];
79
80 if args.ctx.no_engine_commands().await {
81 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Union, Some(solid_out_id));
82 return Ok(new_solids);
83 }
84
85 exec_state
87 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
88 .await?;
89
90 let result = exec_state
91 .send_modeling_cmd(
92 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
93 ModelingCmd::from(
94 mcmd::BooleanUnion::builder()
95 .use_legacy(csg_algorithm.is_legacy())
96 .solid_ids(solids.iter().map(|s| s.id).collect())
97 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
98 .build(),
99 ),
100 )
101 .await?;
102
103 let OkWebSocketResponseData::Modeling {
104 modeling_response: OkModelingCmdResponse::BooleanUnion(boolean_resp),
105 } = result
106 else {
107 return Err(KclError::new_internal(KclErrorDetails::new(
108 "Failed to get the result of the union operation.".to_string(),
109 vec![args.source_range],
110 )));
111 };
112
113 if !boolean_resp.any_intersections {
114 exec_state.warn(
115 CompilationIssue::err(
116 args.source_range,
117 "The bodies in this union had no overlap. This usually indicates a problem in your model, these bodies were probably intended to intersect somewhere.".to_string(),
118 ),
119 annotations::WARN_CSG_NO_INTERSECTION,
120 );
121 }
122
123 for extra_solid_id in boolean_resp.extra_solid_ids {
125 if extra_solid_id == solid_out_id {
126 continue;
127 }
128 let mut new_solid = solid.clone();
129 new_solid.set_id(extra_solid_id);
130 new_solid.artifact_id = extra_solid_id.into();
131 new_solids.push(new_solid);
132 }
133
134 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Union, Some(solid_out_id));
135
136 Ok(new_solids)
137}
138
139pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
142 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
143 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
144 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
145 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
146
147 if solids.len() < 2 {
148 return Err(KclError::new_semantic(KclErrorDetails::new(
149 "At least two solids are required for an intersect operation.".to_string(),
150 vec![args.source_range],
151 )));
152 }
153
154 let solids = inner_intersect(solids, tolerance, csg_algorithm, exec_state, args).await?;
155 Ok(solids.into())
156}
157
158pub(crate) async fn inner_intersect(
159 solids: Vec<Solid>,
160 tolerance: Option<TyF64>,
161 csg_algorithm: CsgAlgorithm,
162 exec_state: &mut ExecState,
163 args: Args,
164) -> Result<Vec<Solid>, KclError> {
165 validate_solids_not_consumed(&solids, exec_state, args.source_range)?;
166
167 let solid_out_id = exec_state.next_uuid();
168
169 let mut solid = solids[0].clone();
170 solid.set_id(solid_out_id);
171 solid.artifact_id = solid_out_id.into();
172 let mut new_solids = vec![solid.clone()];
173
174 if args.ctx.no_engine_commands().await {
175 record_consumed_solids(
176 exec_state,
177 &solids,
178 ConsumedSolidOperation::Intersect,
179 Some(solid_out_id),
180 );
181 return Ok(new_solids);
182 }
183
184 exec_state
186 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
187 .await?;
188
189 let result = exec_state
190 .send_modeling_cmd(
191 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
192 ModelingCmd::from(
193 mcmd::BooleanIntersection::builder()
194 .use_legacy(csg_algorithm.is_legacy())
195 .solid_ids(solids.iter().map(|s| s.id).collect())
196 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
197 .build(),
198 ),
199 )
200 .await?;
201
202 let OkWebSocketResponseData::Modeling {
203 modeling_response: OkModelingCmdResponse::BooleanIntersection(boolean_resp),
204 } = result
205 else {
206 return Err(KclError::new_internal(KclErrorDetails::new(
207 "Failed to get the result of the intersection operation.".to_string(),
208 vec![args.source_range],
209 )));
210 };
211 if !boolean_resp.any_intersections {
212 exec_state.warn(
213 CompilationIssue::err(
214 args.source_range,
215 "The bodies in this intersection had no overlap. This usually indicates a problem in your model, these bodies were probably intended to intersect somewhere.".to_string(),
216 ),
217 annotations::WARN_CSG_NO_INTERSECTION,
218 );
219 }
220
221 for extra_solid_id in boolean_resp.extra_solid_ids {
223 if extra_solid_id == solid_out_id {
224 continue;
225 }
226 let mut new_solid = solid.clone();
227 new_solid.set_id(extra_solid_id);
228 new_solid.artifact_id = extra_solid_id.into();
229 new_solids.push(new_solid);
230 }
231
232 record_consumed_solids(
233 exec_state,
234 &solids,
235 ConsumedSolidOperation::Intersect,
236 Some(solid_out_id),
237 );
238
239 Ok(new_solids)
240}
241
242pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
244 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
245 let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
246
247 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
248 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
249 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
250
251 let solids = inner_subtract(solids, tools, tolerance, csg_algorithm, exec_state, args).await?;
252 Ok(solids.into())
253}
254
255pub(crate) async fn inner_subtract(
256 solids: Vec<Solid>,
257 tools: Vec<Solid>,
258 tolerance: Option<TyF64>,
259 csg_algorithm: CsgAlgorithm,
260 exec_state: &mut ExecState,
261 args: Args,
262) -> Result<Vec<Solid>, KclError> {
263 let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
264 validate_solids_not_consumed(&combined_solids, exec_state, args.source_range)?;
265
266 let solid_out_id = exec_state.next_uuid();
267
268 let mut solid = solids[0].clone();
269 solid.set_id(solid_out_id);
270 solid.artifact_id = solid_out_id.into();
271 let mut new_solids = vec![solid.clone()];
272
273 if args.ctx.no_engine_commands().await {
274 record_consumed_solids(
275 exec_state,
276 &solids,
277 ConsumedSolidOperation::Subtract,
278 Some(solid_out_id),
279 );
280 record_consumed_solids(exec_state, &tools, ConsumedSolidOperation::Subtract, None);
281 return Ok(new_solids);
282 }
283
284 exec_state
286 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
287 .await?;
288
289 let result = exec_state
290 .send_modeling_cmd(
291 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
292 ModelingCmd::from(
293 mcmd::BooleanSubtract::builder()
294 .use_legacy(csg_algorithm.is_legacy())
295 .target_ids(solids.iter().map(|s| s.id).collect())
296 .tool_ids(tools.iter().map(|s| s.id).collect())
297 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
298 .build(),
299 ),
300 )
301 .await?;
302
303 let OkWebSocketResponseData::Modeling {
304 modeling_response: OkModelingCmdResponse::BooleanSubtract(boolean_resp),
305 } = result
306 else {
307 return Err(KclError::new_internal(KclErrorDetails::new(
308 "Failed to get the result of the subtract operation.".to_string(),
309 vec![args.source_range],
310 )));
311 };
312
313 if !boolean_resp.any_intersections {
314 exec_state.warn(
315 CompilationIssue::err(
316 args.source_range,
317 "The bodies in this subtraction had no overlap. This usually indicates a problem in your model, these bodies were probably intended to intersect somewhere.".to_string(),
318 ),
319 annotations::WARN_CSG_NO_INTERSECTION,
320 );
321 }
322
323 for extra_solid_id in boolean_resp.extra_solid_ids {
325 if extra_solid_id == solid_out_id {
326 continue;
327 }
328 let mut new_solid = solid.clone();
329 new_solid.set_id(extra_solid_id);
330 new_solid.artifact_id = extra_solid_id.into();
331 new_solids.push(new_solid);
332 }
333
334 record_consumed_solids(
335 exec_state,
336 &solids,
337 ConsumedSolidOperation::Subtract,
338 Some(solid_out_id),
339 );
340 record_consumed_solids(exec_state, &tools, ConsumedSolidOperation::Subtract, None);
341
342 Ok(new_solids)
343}
344
345pub async fn split(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
347 let targets: Vec<Solid> = args.get_unlabeled_kw_arg("targets", &RuntimeType::solids(), exec_state)?;
348 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
349 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
350 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
351 let tools: Option<Vec<Solid>> = args.get_kw_arg_opt("tools", &RuntimeType::solids(), exec_state)?;
352 let keep_tools = args
353 .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
354 .unwrap_or_default();
355 let merge = args
356 .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
357 .unwrap_or_default();
358
359 if targets.is_empty() {
360 return Err(KclError::new_semantic(KclErrorDetails::new(
361 "At least one target body is required.".to_string(),
362 vec![args.source_range],
363 )));
364 }
365
366 let body = inner_imprint(
367 targets,
368 tools,
369 keep_tools,
370 merge,
371 tolerance,
372 csg_algorithm,
373 exec_state,
374 args,
375 )
376 .await?;
377 Ok(body.into())
378}
379
380#[allow(clippy::too_many_arguments)]
381pub(crate) async fn inner_imprint(
382 targets: Vec<Solid>,
383 tools: Option<Vec<Solid>>,
384 keep_tools: bool,
385 merge: bool,
386 tolerance: Option<TyF64>,
387 csg_algorithm: CsgAlgorithm,
388 exec_state: &mut ExecState,
389 args: Args,
390) -> Result<Vec<Solid>, KclError> {
391 validate_solids_not_consumed(&targets, exec_state, args.source_range)?;
392 if let Some(tools) = tools.as_ref() {
393 validate_solids_not_consumed(tools, exec_state, args.source_range)?;
394 }
395
396 let body_out_id = exec_state.next_uuid();
397
398 let mut body = targets[0].clone();
399 body.set_id(body_out_id);
400 body.artifact_id = body_out_id.into();
401 let mut new_solids = vec![body.clone()];
402 let separate_bodies = !merge;
403
404 if args.ctx.no_engine_commands().await {
405 if separate_bodies {
406 let extra_solid_id = exec_state.next_uuid();
407 let mut new_solid = body.clone();
408 new_solid.set_id(extra_solid_id);
409 new_solid.artifact_id = extra_solid_id.into();
410 new_solids.push(new_solid);
411 }
412 record_consumed_solids(exec_state, &targets, ConsumedSolidOperation::Split, Some(body_out_id));
413 if !keep_tools && let Some(tools) = tools.as_ref() {
414 record_consumed_solids(exec_state, tools, ConsumedSolidOperation::Split, None);
415 }
416 return Ok(new_solids);
417 }
418
419 let mut imprint_solids = targets.clone();
421 if let Some(tool_solids) = tools.as_ref() {
422 imprint_solids.extend_from_slice(tool_solids);
423 }
424 exec_state
425 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
426 .await?;
427
428 let body_ids = targets.iter().map(|body| body.id).collect();
429 let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
430 let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
431 let imprint_cmd = mcmd::BooleanImprint::builder()
432 .use_legacy(csg_algorithm.is_legacy())
433 .body_ids(body_ids)
434 .tolerance(tolerance)
435 .separate_bodies(separate_bodies)
436 .keep_tools(keep_tools)
437 .maybe_tool_ids(tool_ids)
438 .build();
439 let result = exec_state
440 .send_modeling_cmd(
441 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
442 ModelingCmd::from(imprint_cmd),
443 )
444 .await?;
445
446 let OkWebSocketResponseData::Modeling {
447 modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
448 } = result
449 else {
450 return Err(KclError::new_internal(KclErrorDetails::new(
451 "Failed to get the result of the Imprint operation.".to_string(),
452 vec![args.source_range],
453 )));
454 };
455 if !boolean_resp.any_intersections {
456 exec_state.warn(
457 CompilationIssue::err(
458 args.source_range,
459 "The bodies in this split had no overlap. This usually indicates a problem in your model, these bodies were probably intended to intersect somewhere.".to_string(),
460 ),
461 annotations::WARN_CSG_NO_INTERSECTION,
462 );
463 }
464
465 for extra_solid_id in boolean_resp.extra_solid_ids {
467 if extra_solid_id == body_out_id {
468 continue;
469 }
470 let mut new_solid = body.clone();
471 new_solid.set_id(extra_solid_id);
472 new_solid.artifact_id = extra_solid_id.into();
473 new_solids.push(new_solid);
474 }
475
476 record_consumed_solids(exec_state, &targets, ConsumedSolidOperation::Split, Some(body_out_id));
477 if !keep_tools && let Some(tools) = tools.as_ref() {
478 record_consumed_solids(exec_state, tools, ConsumedSolidOperation::Split, None);
479 }
480
481 Ok(new_solids)
482}
483
484#[cfg(test)]
485mod tests {
486 use crate::errors::KclError;
487 use crate::execution::MockConfig;
488
489 #[tokio::test(flavor = "multi_thread")]
490 async fn subtract_reusing_consumed_target_reports_kcl_error() {
491 let code = r#"
492targetSketch = sketch(on = XY) {
493 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
494 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
495 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
496 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
497 coincident([line1.end, line2.start])
498 coincident([line2.end, line3.start])
499 coincident([line3.end, line4.start])
500 coincident([line4.end, line1.start])
501 equalLength([line1, line2, line3, line4])
502}
503
504target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
505
506tool1Sketch = sketch(on = XY) {
507 line1 = line(start = [var -11, var -11], end = [var -7, var -11])
508 line2 = line(start = [var -7, var -11], end = [var -7, var -7])
509 line3 = line(start = [var -7, var -7], end = [var -11, var -7])
510 line4 = line(start = [var -11, var -7], end = [var -11, var -11])
511 coincident([line1.end, line2.start])
512 coincident([line2.end, line3.start])
513 coincident([line3.end, line4.start])
514 coincident([line4.end, line1.start])
515 equalLength([line1, line2, line3, line4])
516}
517
518tool1 = extrude(region(point = [-9, -9], sketch = tool1Sketch), length = 4)
519
520tool2Sketch = sketch(on = XY) {
521 line1 = line(start = [var 7, var 7], end = [var 11, var 7])
522 line2 = line(start = [var 11, var 7], end = [var 11, var 11])
523 line3 = line(start = [var 11, var 11], end = [var 7, var 11])
524 line4 = line(start = [var 7, var 11], end = [var 7, var 7])
525 coincident([line1.end, line2.start])
526 coincident([line2.end, line3.start])
527 coincident([line3.end, line4.start])
528 coincident([line4.end, line1.start])
529 equalLength([line1, line2, line3, line4])
530}
531
532tool2 = extrude(region(point = [9, 9], sketch = tool2Sketch), length = 4)
533
534first = subtract(target, tools = [tool1])
535second = subtract(target, tools = [tool2])
536"#;
537
538 let ctx = crate::ExecutorContext::new_mock(None).await;
539 let program = crate::Program::parse_no_errs(code).unwrap();
540 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
541 ctx.close().await;
542
543 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
544 let message = err.error.message();
545 assert!(
546 message.contains("`target` was already consumed by a `subtract` operation"),
547 "{message}"
548 );
549 assert!(message.contains("The operation result is now in `first`"), "{message}");
550 }
551
552 #[tokio::test(flavor = "multi_thread")]
553 async fn subtract_reusing_consumed_tool_reports_kcl_error() {
554 let code = r#"
555targetSketch = sketch(on = XY) {
556 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
557 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
558 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
559 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
560 coincident([line1.end, line2.start])
561 coincident([line2.end, line3.start])
562 coincident([line3.end, line4.start])
563 coincident([line4.end, line1.start])
564 equalLength([line1, line2, line3, line4])
565}
566
567target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
568
569toolSketch = sketch(on = XY) {
570 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
571 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
572 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
573 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
574 coincident([line1.end, line2.start])
575 coincident([line2.end, line3.start])
576 coincident([line3.end, line4.start])
577 coincident([line4.end, line1.start])
578 equalLength([line1, line2, line3, line4])
579}
580
581tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
582
583first = subtract(target, tools = [tool])
584second = subtract(first, tools = [tool])
585"#;
586
587 let ctx = crate::ExecutorContext::new_mock(None).await;
588 let program = crate::Program::parse_no_errs(code).unwrap();
589 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
590 ctx.close().await;
591
592 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
593 let message = err.error.message();
594 assert!(
595 message.contains("`tool` was already consumed by a `subtract` operation"),
596 "{message}"
597 );
598 assert!(message.contains("can no longer be used"), "{message}");
599 }
600
601 #[tokio::test(flavor = "multi_thread")]
602 async fn union_reusing_consumed_solid_reports_kcl_error() {
603 let code = r#"
604leftSketch = sketch(on = XY) {
605 line1 = line(start = [var -10, var -10], end = [var -2, var -10])
606 line2 = line(start = [var -2, var -10], end = [var -2, var -2])
607 line3 = line(start = [var -2, var -2], end = [var -10, var -2])
608 line4 = line(start = [var -10, var -2], end = [var -10, var -10])
609 coincident([line1.end, line2.start])
610 coincident([line2.end, line3.start])
611 coincident([line3.end, line4.start])
612 coincident([line4.end, line1.start])
613 equalLength([line1, line2, line3, line4])
614}
615
616left = extrude(region(point = [-6, -6], sketch = leftSketch), length = 8)
617
618rightSketch = sketch(on = XY) {
619 line1 = line(start = [var -2, var -2], end = [var 6, var -2])
620 line2 = line(start = [var 6, var -2], end = [var 6, var 6])
621 line3 = line(start = [var 6, var 6], end = [var -2, var 6])
622 line4 = line(start = [var -2, var 6], end = [var -2, var -2])
623 coincident([line1.end, line2.start])
624 coincident([line2.end, line3.start])
625 coincident([line3.end, line4.start])
626 coincident([line4.end, line1.start])
627 equalLength([line1, line2, line3, line4])
628}
629
630right = extrude(region(point = [2, 2], sketch = rightSketch), length = 8)
631
632toolSketch = sketch(on = XY) {
633 line1 = line(start = [var -1, var -1], end = [var 1, var -1])
634 line2 = line(start = [var 1, var -1], end = [var 1, var 1])
635 line3 = line(start = [var 1, var 1], end = [var -1, var 1])
636 line4 = line(start = [var -1, var 1], end = [var -1, var -1])
637 coincident([line1.end, line2.start])
638 coincident([line2.end, line3.start])
639 coincident([line3.end, line4.start])
640 coincident([line4.end, line1.start])
641 equalLength([line1, line2, line3, line4])
642}
643
644tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 2)
645
646first = union([left, right])
647second = union([first, tool])
648third = subtract(left, tools = [tool])
649"#;
650
651 let ctx = crate::ExecutorContext::new_mock(None).await;
652 let program = crate::Program::parse_no_errs(code).unwrap();
653 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
654 ctx.close().await;
655
656 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
657 let message = err.error.message();
658 assert!(
659 message.contains("`left` was already consumed by a `union` operation"),
660 "{message}"
661 );
662 assert!(message.contains("The operation result is now in `second`"), "{message}");
663 }
664
665 #[tokio::test(flavor = "multi_thread")]
666 async fn intersect_reusing_consumed_solid_reports_kcl_error() {
667 let code = r#"
668leftSketch = sketch(on = XY) {
669 line1 = line(start = [var -10, var -10], end = [var 4, var -10])
670 line2 = line(start = [var 4, var -10], end = [var 4, var 4])
671 line3 = line(start = [var 4, var 4], end = [var -10, var 4])
672 line4 = line(start = [var -10, var 4], end = [var -10, var -10])
673 coincident([line1.end, line2.start])
674 coincident([line2.end, line3.start])
675 coincident([line3.end, line4.start])
676 coincident([line4.end, line1.start])
677 equalLength([line1, line2, line3, line4])
678}
679
680left = extrude(region(point = [-3, -3], sketch = leftSketch), length = 8)
681
682rightSketch = sketch(on = XY) {
683 line1 = line(start = [var -4, var -4], end = [var 10, var -4])
684 line2 = line(start = [var 10, var -4], end = [var 10, var 10])
685 line3 = line(start = [var 10, var 10], end = [var -4, var 10])
686 line4 = line(start = [var -4, var 10], end = [var -4, var -4])
687 coincident([line1.end, line2.start])
688 coincident([line2.end, line3.start])
689 coincident([line3.end, line4.start])
690 coincident([line4.end, line1.start])
691 equalLength([line1, line2, line3, line4])
692}
693
694right = extrude(region(point = [3, 3], sketch = rightSketch), length = 8)
695
696toolSketch = sketch(on = XY) {
697 line1 = line(start = [var -1, var -1], end = [var 1, var -1])
698 line2 = line(start = [var 1, var -1], end = [var 1, var 1])
699 line3 = line(start = [var 1, var 1], end = [var -1, var 1])
700 line4 = line(start = [var -1, var 1], end = [var -1, var -1])
701 coincident([line1.end, line2.start])
702 coincident([line2.end, line3.start])
703 coincident([line3.end, line4.start])
704 coincident([line4.end, line1.start])
705 equalLength([line1, line2, line3, line4])
706}
707
708tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 2)
709
710first = intersect([left, right])
711second = subtract(left, tools = [tool])
712"#;
713
714 let ctx = crate::ExecutorContext::new_mock(None).await;
715 let program = crate::Program::parse_no_errs(code).unwrap();
716 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
717 ctx.close().await;
718
719 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
720 let message = err.error.message();
721 assert!(
722 message.contains("`left` was already consumed by an `intersect` operation"),
723 "{message}"
724 );
725 assert!(message.contains("The operation result is now in `first`"), "{message}");
726 }
727
728 #[tokio::test(flavor = "multi_thread")]
729 async fn split_keep_tools_does_not_consume_tools() {
730 let code = r#"
731targetSketch = sketch(on = XY) {
732 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
733 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
734 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
735 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
736 coincident([line1.end, line2.start])
737 coincident([line2.end, line3.start])
738 coincident([line3.end, line4.start])
739 coincident([line4.end, line1.start])
740 equalLength([line1, line2, line3, line4])
741}
742
743target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
744
745toolSketch = sketch(on = XY) {
746 line1 = line(start = [var -2, var -10], end = [var 2, var -10])
747 line2 = line(start = [var 2, var -10], end = [var 2, var 10])
748 line3 = line(start = [var 2, var 10], end = [var -2, var 10])
749 line4 = line(start = [var -2, var 10], end = [var -2, var -10])
750 coincident([line1.end, line2.start])
751 coincident([line2.end, line3.start])
752 coincident([line3.end, line4.start])
753 coincident([line4.end, line1.start])
754}
755
756tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 20)
757
758first = split(target, tools = [tool], keepTools = true)
759second = subtract(first, tools = [tool])
760"#;
761
762 let ctx = crate::ExecutorContext::new_mock(None).await;
763 let program = crate::Program::parse_no_errs(code).unwrap();
764 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
765 ctx.close().await;
766
767 assert!(outcome.variables.contains_key("second"));
768 }
769
770 #[tokio::test(flavor = "multi_thread")]
771 async fn split_without_keep_tools_consumes_tools() {
772 let code = r#"
773targetSketch = sketch(on = XY) {
774 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
775 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
776 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
777 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
778 coincident([line1.end, line2.start])
779 coincident([line2.end, line3.start])
780 coincident([line3.end, line4.start])
781 coincident([line4.end, line1.start])
782 equalLength([line1, line2, line3, line4])
783}
784
785target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
786
787toolSketch = sketch(on = XY) {
788 line1 = line(start = [var -2, var -10], end = [var 2, var -10])
789 line2 = line(start = [var 2, var -10], end = [var 2, var 10])
790 line3 = line(start = [var 2, var 10], end = [var -2, var 10])
791 line4 = line(start = [var -2, var 10], end = [var -2, var -10])
792 coincident([line1.end, line2.start])
793 coincident([line2.end, line3.start])
794 coincident([line3.end, line4.start])
795 coincident([line4.end, line1.start])
796}
797
798tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 20)
799
800first = split(target, tools = [tool])
801second = subtract(first, tools = [tool])
802"#;
803
804 let ctx = crate::ExecutorContext::new_mock(None).await;
805 let program = crate::Program::parse_no_errs(code).unwrap();
806 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
807 ctx.close().await;
808
809 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
810 let message = err.error.message();
811 assert!(
812 message.contains("`tool` was already consumed by a `split` operation"),
813 "{message}"
814 );
815 assert!(message.contains("can no longer be used"), "{message}");
816 }
817}