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
64fn is_single_target_self_subtract(target_ids: &[uuid::Uuid], tool_ids: &[uuid::Uuid]) -> bool {
65 target_ids.len() == 1 && tool_ids.len() == 1 && target_ids[0] == tool_ids[0]
66}
67
68fn subtract_output_ids(
69 solid_out_id: uuid::Uuid,
70 target_ids: &[uuid::Uuid],
71 tool_ids: &[uuid::Uuid],
72 extra_solid_ids: &[uuid::Uuid],
73) -> Vec<uuid::Uuid> {
74 if is_single_target_self_subtract(target_ids, tool_ids) {
75 return Vec::new();
76 }
77
78 let mut output_ids = if target_ids.len() == 1 {
79 vec![solid_out_id]
80 } else {
81 Vec::new()
82 };
83
84 for extra_solid_id in extra_solid_ids {
85 if !output_ids.contains(extra_solid_id) {
86 output_ids.push(*extra_solid_id);
87 }
88 }
89
90 output_ids
91}
92
93fn inherit_face_tags(output: &mut Solid, inputs: &[Solid]) {
94 for input in inputs {
95 for (name, tag) in &input.faces {
96 output.faces.entry(name.clone()).or_insert_with(|| tag.clone());
98 }
99 }
100}
101
102pub(crate) async fn inner_union(
103 solids: Vec<Solid>,
104 tolerance: Option<TyF64>,
105 csg_algorithm: CsgAlgorithm,
106 exec_state: &mut ExecState,
107 args: Args,
108) -> Result<Vec<Solid>, KclError> {
109 validate_solids_not_consumed(&solids, exec_state, args.source_range)?;
110
111 let solid_out_id = exec_state.next_uuid();
112
113 let mut solid = solids[0].clone();
114 inherit_face_tags(&mut solid, &solids);
115 solid.set_id(solid_out_id);
116 solid.become_new_body(solid_out_id, solid_out_id.into());
117 let mut new_solids = vec![solid.clone()];
118
119 if args.ctx.no_engine_commands().await {
120 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Union, &new_solids);
121 return Ok(new_solids);
122 }
123
124 exec_state
126 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
127 .await?;
128
129 let result = exec_state
130 .send_modeling_cmd(
131 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
132 ModelingCmd::from(
133 mcmd::BooleanUnion::builder()
134 .use_legacy(csg_algorithm.is_legacy())
135 .solid_ids(solids.iter().map(|s| s.id).collect())
136 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
137 .build(),
138 ),
139 )
140 .await?;
141
142 let OkWebSocketResponseData::Modeling {
143 modeling_response: OkModelingCmdResponse::BooleanUnion(boolean_resp),
144 } = result
145 else {
146 return Err(KclError::new_internal(KclErrorDetails::new(
147 "Failed to get the result of the union operation.".to_string(),
148 vec![args.source_range],
149 )));
150 };
151
152 if !boolean_resp.any_intersections {
153 exec_state.warn(
154 CompilationIssue::err(
155 args.source_range,
156 "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(),
157 ),
158 annotations::WARN_CSG_NO_INTERSECTION,
159 );
160 }
161
162 for extra_solid_id in boolean_resp.extra_solid_ids {
164 if extra_solid_id == solid_out_id {
165 continue;
166 }
167 let mut new_solid = solid.clone();
168 new_solid.set_id(extra_solid_id);
169 new_solid.value_id = solid_out_id;
170 new_solid.become_new_body(extra_solid_id, extra_solid_id.into());
171 new_solids.push(new_solid);
172 }
173
174 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Union, &new_solids);
175
176 Ok(new_solids)
177}
178
179pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
182 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
183 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
184 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
185 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
186
187 if solids.len() < 2 {
188 return Err(KclError::new_semantic(KclErrorDetails::new(
189 "At least two solids are required for an intersect operation.".to_string(),
190 vec![args.source_range],
191 )));
192 }
193
194 let solids = inner_intersect(solids, tolerance, csg_algorithm, exec_state, args).await?;
195 Ok(solids.into())
196}
197
198pub(crate) async fn inner_intersect(
199 solids: Vec<Solid>,
200 tolerance: Option<TyF64>,
201 csg_algorithm: CsgAlgorithm,
202 exec_state: &mut ExecState,
203 args: Args,
204) -> Result<Vec<Solid>, KclError> {
205 validate_solids_not_consumed(&solids, exec_state, args.source_range)?;
206
207 let solid_out_id = exec_state.next_uuid();
208
209 let mut solid = solids[0].clone();
210 inherit_face_tags(&mut solid, &solids);
211 solid.set_id(solid_out_id);
212 solid.become_new_body(solid_out_id, solid_out_id.into());
213 let mut new_solids = vec![solid.clone()];
214
215 if args.ctx.no_engine_commands().await {
216 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Intersect, &new_solids);
217 return Ok(new_solids);
218 }
219
220 exec_state
222 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
223 .await?;
224
225 let result = exec_state
226 .send_modeling_cmd(
227 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
228 ModelingCmd::from(
229 mcmd::BooleanIntersection::builder()
230 .use_legacy(csg_algorithm.is_legacy())
231 .solid_ids(solids.iter().map(|s| s.id).collect())
232 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
233 .build(),
234 ),
235 )
236 .await?;
237
238 let OkWebSocketResponseData::Modeling {
239 modeling_response: OkModelingCmdResponse::BooleanIntersection(boolean_resp),
240 } = result
241 else {
242 return Err(KclError::new_internal(KclErrorDetails::new(
243 "Failed to get the result of the intersection operation.".to_string(),
244 vec![args.source_range],
245 )));
246 };
247 if !boolean_resp.any_intersections {
248 exec_state.warn(
249 CompilationIssue::err(
250 args.source_range,
251 "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(),
252 ),
253 annotations::WARN_CSG_NO_INTERSECTION,
254 );
255 }
256
257 for extra_solid_id in boolean_resp.extra_solid_ids {
259 if extra_solid_id == solid_out_id {
260 continue;
261 }
262 let mut new_solid = solid.clone();
263 new_solid.set_id(extra_solid_id);
264 new_solid.value_id = solid_out_id;
265 new_solid.become_new_body(extra_solid_id, extra_solid_id.into());
266 new_solids.push(new_solid);
267 }
268
269 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Intersect, &new_solids);
270
271 Ok(new_solids)
272}
273
274pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
276 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
277 let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
278
279 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
280 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
281 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
282
283 let solids = inner_subtract(solids, tools, tolerance, csg_algorithm, exec_state, args).await?;
284 Ok(solids.into())
285}
286
287pub(crate) async fn inner_subtract(
288 solids: Vec<Solid>,
289 tools: Vec<Solid>,
290 tolerance: Option<TyF64>,
291 csg_algorithm: CsgAlgorithm,
292 exec_state: &mut ExecState,
293 args: Args,
294) -> Result<Vec<Solid>, KclError> {
295 let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
296 validate_solids_not_consumed(&combined_solids, exec_state, args.source_range)?;
297
298 let solid_out_id = exec_state.next_uuid();
299 let target_ids = solids.iter().map(|s| s.id).collect::<Vec<_>>();
300 let tool_ids = tools.iter().map(|s| s.id).collect::<Vec<_>>();
301
302 if args.ctx.no_engine_commands().await {
303 let new_solids = solids
305 .iter()
306 .enumerate()
307 .map(|(index, solid)| {
308 let output_id = if index == 0 {
311 solid_out_id
312 } else {
313 exec_state.next_uuid()
314 };
315 let mut new_solid = solid.clone();
316 new_solid.set_id(output_id);
317 new_solid.become_new_body(output_id, output_id.into());
318 new_solid
319 })
320 .collect::<Vec<_>>();
321 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Subtract, &new_solids);
322 record_consumed_solids(exec_state, &tools, ConsumedSolidOperation::Subtract, &[]);
323 return Ok(new_solids);
324 }
325
326 exec_state
328 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
329 .await?;
330
331 let result = exec_state
332 .send_modeling_cmd(
333 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
334 ModelingCmd::from(
335 mcmd::BooleanSubtract::builder()
336 .use_legacy(csg_algorithm.is_legacy())
337 .target_ids(target_ids.clone())
338 .tool_ids(tool_ids.clone())
339 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
340 .build(),
341 ),
342 )
343 .await?;
344
345 let OkWebSocketResponseData::Modeling {
346 modeling_response: OkModelingCmdResponse::BooleanSubtract(boolean_resp),
347 } = result
348 else {
349 return Err(KclError::new_internal(KclErrorDetails::new(
350 "Failed to get the result of the subtract operation.".to_string(),
351 vec![args.source_range],
352 )));
353 };
354
355 if !boolean_resp.any_intersections {
356 exec_state.warn(
357 CompilationIssue::err(
358 args.source_range,
359 "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(),
360 ),
361 annotations::WARN_CSG_NO_INTERSECTION,
362 );
363 }
364
365 let output_ids = subtract_output_ids(solid_out_id, &target_ids, &tool_ids, &boolean_resp.extra_solid_ids);
366 let new_solids = output_ids
367 .into_iter()
368 .map(|output_id| {
369 let mut new_solid = solids[0].clone();
370 new_solid.set_id(output_id);
371 new_solid.value_id = solid_out_id;
372 new_solid.become_new_body(output_id, output_id.into());
373 new_solid
374 })
375 .collect::<Vec<_>>();
376
377 record_consumed_solids(exec_state, &solids, ConsumedSolidOperation::Subtract, &new_solids);
378 record_consumed_solids(exec_state, &tools, ConsumedSolidOperation::Subtract, &[]);
379
380 Ok(new_solids)
381}
382
383pub async fn split(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
385 let targets: Vec<Solid> = args.get_unlabeled_kw_arg("targets", &RuntimeType::solids(), exec_state)?;
386 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
387 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
388 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
389 let tools: Option<Vec<Solid>> = args.get_kw_arg_opt("tools", &RuntimeType::solids(), exec_state)?;
390 let keep_tools = args
391 .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
392 .unwrap_or_default();
393 let merge = args
394 .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
395 .unwrap_or_default();
396
397 if targets.is_empty() {
398 return Err(KclError::new_semantic(KclErrorDetails::new(
399 "At least one target body is required.".to_string(),
400 vec![args.source_range],
401 )));
402 }
403
404 let body = inner_imprint(
405 targets,
406 tools,
407 keep_tools,
408 merge,
409 tolerance,
410 csg_algorithm,
411 exec_state,
412 args,
413 )
414 .await?;
415 Ok(body.into())
416}
417
418#[allow(clippy::too_many_arguments)]
419pub(crate) async fn inner_imprint(
420 targets: Vec<Solid>,
421 tools: Option<Vec<Solid>>,
422 keep_tools: bool,
423 merge: bool,
424 tolerance: Option<TyF64>,
425 csg_algorithm: CsgAlgorithm,
426 exec_state: &mut ExecState,
427 args: Args,
428) -> Result<Vec<Solid>, KclError> {
429 validate_solids_not_consumed(&targets, exec_state, args.source_range)?;
430 if let Some(tools) = tools.as_ref() {
431 validate_solids_not_consumed(tools, exec_state, args.source_range)?;
432 }
433
434 let body_out_id = exec_state.next_uuid();
435
436 let mut body = targets[0].clone();
437 body.set_id(body_out_id);
438 body.become_new_body(body_out_id, body_out_id.into());
439 let mut new_solids = vec![body.clone()];
440 let separate_bodies = !merge;
441
442 if args.ctx.no_engine_commands().await {
443 if separate_bodies {
444 let extra_solid_id = exec_state.next_uuid();
445 let mut new_solid = body.clone();
446 new_solid.set_id(extra_solid_id);
447 new_solid.value_id = body_out_id;
448 new_solid.become_new_body(extra_solid_id, extra_solid_id.into());
449 new_solids.push(new_solid);
450 }
451 record_consumed_solids(exec_state, &targets, ConsumedSolidOperation::Split, &new_solids);
452 if !keep_tools && let Some(tools) = tools.as_ref() {
453 record_consumed_solids(exec_state, tools, ConsumedSolidOperation::Split, &[]);
454 }
455 return Ok(new_solids);
456 }
457
458 let mut imprint_solids = targets.clone();
460 if let Some(tool_solids) = tools.as_ref() {
461 imprint_solids.extend_from_slice(tool_solids);
462 }
463 exec_state
464 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
465 .await?;
466
467 let body_ids = targets.iter().map(|body| body.id).collect();
468 let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
469 let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
470 let imprint_cmd = mcmd::BooleanImprint::builder()
471 .use_legacy(csg_algorithm.is_legacy())
472 .body_ids(body_ids)
473 .tolerance(tolerance)
474 .separate_bodies(separate_bodies)
475 .keep_tools(keep_tools)
476 .maybe_tool_ids(tool_ids)
477 .build();
478 let result = exec_state
479 .send_modeling_cmd(
480 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
481 ModelingCmd::from(imprint_cmd),
482 )
483 .await?;
484
485 let OkWebSocketResponseData::Modeling {
486 modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
487 } = result
488 else {
489 return Err(KclError::new_internal(KclErrorDetails::new(
490 "Failed to get the result of the Imprint operation.".to_string(),
491 vec![args.source_range],
492 )));
493 };
494 if !boolean_resp.any_intersections {
495 exec_state.warn(
496 CompilationIssue::err(
497 args.source_range,
498 "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(),
499 ),
500 annotations::WARN_CSG_NO_INTERSECTION,
501 );
502 }
503
504 for extra_solid_id in boolean_resp.extra_solid_ids {
506 if extra_solid_id == body_out_id {
507 continue;
508 }
509 let mut new_solid = body.clone();
510 new_solid.set_id(extra_solid_id);
511 new_solid.value_id = body_out_id;
512 new_solid.become_new_body(extra_solid_id, extra_solid_id.into());
513 new_solids.push(new_solid);
514 }
515
516 record_consumed_solids(exec_state, &targets, ConsumedSolidOperation::Split, &new_solids);
517 if !keep_tools && let Some(tools) = tools.as_ref() {
518 record_consumed_solids(exec_state, tools, ConsumedSolidOperation::Split, &[]);
519 }
520
521 Ok(new_solids)
522}
523
524#[cfg(test)]
525mod tests {
526 use uuid::Uuid;
527
528 use super::subtract_output_ids;
529 use crate::errors::KclError;
530 use crate::execution::KclValue;
531 use crate::execution::MockConfig;
532 use crate::execution::parse_execute;
533
534 async fn assert_csg_inherits_face_tags(operation: &str) {
535 let inputs = r#"@settings(kclVersion = 2.0)
536fn profile(@plane) {
537 return sketch(on = plane) {
538 bottom = line(start = [-10mm, -10mm], end = [10mm, -10mm])
539 right = line(start = [10mm, -10mm], end = [10mm, 10mm])
540 top = line(start = [10mm, 10mm], end = [-10mm, 10mm])
541 left = line(start = [-10mm, 10mm], end = [-10mm, -10mm])
542 }
543}
544firstProfile = profile(XY)
545secondProfile = profile(YZ)
546thirdProfile = profile(XZ)
547firstRegion = region(segments = [firstProfile.bottom])
548secondRegion = region(segments = [secondProfile.bottom])
549thirdRegion = region(segments = [thirdProfile.bottom])
550first = extrude(firstRegion, length = 5mm, symmetric = true, tagEnd = $firstEnd)
551second = extrude(secondRegion, length = 5mm, symmetric = true, tagEnd = $secondEnd)
552third = extrude(thirdRegion, length = 5mm, symmetric = true, tagEnd = $thirdEnd)
553untagged = extrude(region(segments = [firstProfile.bottom]), length = 5mm, symmetric = true)
554"#;
555 for (input_names, tag_names) in [
556 ("first, second", &["first", "second"][..]),
557 ("first, second, third", &["first", "second", "third"]),
558 ("third, second, first", &["third", "second", "first"]),
559 ("untagged, second", &["second"]),
560 ] {
561 let mut code = inputs.to_owned();
562 for name in tag_names {
563 code.push_str(&format!("{name}Original = {name}.faces.{name}End\n"));
564 }
565 code.push_str(&format!("body = {operation}([{input_names}])\n"));
566 for name in tag_names {
567 code.push_str(&format!("{name}FromBody = body.faces.{name}End\n"));
568 }
569 let result = parse_execute(&code).await.unwrap();
570 for name in tag_names {
571 let output_tag = result.variable(&format!("{name}FromBody"));
572 assert!(matches!(&output_tag, KclValue::TagIdentifier(_)));
573 assert_eq!(
574 output_tag,
575 result.variable(&format!("{name}Original")),
576 "{operation}: {name}"
577 );
578 }
579 }
580 }
581
582 #[tokio::test(flavor = "multi_thread")]
583 async fn union_inherits_face_tags_from_all_inputs() {
584 assert_csg_inherits_face_tags("union").await;
585 }
586
587 #[tokio::test(flavor = "multi_thread")]
588 async fn intersect_inherits_face_tags_from_all_inputs() {
589 assert_csg_inherits_face_tags("intersect").await;
590 }
591
592 #[tokio::test(flavor = "multi_thread")]
593 async fn csg_keeps_first_input_for_duplicate_face_tag_names() {
594 let inputs = r#"@settings(kclVersion = 2.0)
595fn body(@plane) {
596 profile = sketch(on = plane) {
597 circle1 = circle(center = [0mm, 0mm], start = [10mm, 0mm])
598 }
599 return extrude(region(segments = [profile.circle1]), length = 5mm, tagEnd = $cap)
600}
601first = body(XY)
602second = body(YZ)
603firstCap = first.faces.cap
604secondCap = second.faces.cap
605"#;
606 for operation in ["union", "intersect"] {
607 for (inputs_order, expected, other) in [
608 ("first, second", "firstCap", "secondCap"),
609 ("second, first", "secondCap", "firstCap"),
610 ] {
611 let code =
612 format!("{inputs}\ncombined = {operation}([{inputs_order}])\nselected = combined.faces.cap\n");
613 let result = parse_execute(&code).await.unwrap();
614 assert_eq!(result.variable("selected"), result.variable(expected));
615 assert_ne!(result.variable("selected"), result.variable(other));
616 }
617 }
618 }
619
620 fn test_uuid(id: u128) -> Uuid {
621 Uuid::from_u128(id)
622 }
623
624 #[test]
625 fn subtract_output_ids_single_target_uses_command_id() {
626 let output_id = test_uuid(100);
627 let target_id = test_uuid(1);
628 let tool_id = test_uuid(2);
629 let extra_id = test_uuid(3);
630
631 let output_ids = subtract_output_ids(output_id, &[target_id], &[tool_id], &[extra_id]);
632
633 assert_eq!(output_ids, vec![output_id, extra_id]);
634 }
635
636 #[test]
637 fn subtract_output_ids_multi_target_uses_response_ids_only() {
638 let output_id = test_uuid(100);
639 let target_ids = [test_uuid(1), test_uuid(2)];
640 let tool_id = test_uuid(3);
641 let extra_ids = [test_uuid(4), test_uuid(5)];
642
643 let output_ids = subtract_output_ids(output_id, &target_ids, &[tool_id], &extra_ids);
644
645 assert_eq!(output_ids, extra_ids);
646 }
647
648 #[test]
649 fn subtract_output_ids_self_subtract_returns_no_outputs() {
650 let output_id = test_uuid(100);
651 let target_id = test_uuid(1);
652
653 let output_ids = subtract_output_ids(output_id, &[target_id], &[target_id], &[]);
654
655 assert!(output_ids.is_empty());
656 }
657
658 #[tokio::test(flavor = "multi_thread")]
659 async fn subtract_reusing_consumed_target_reports_kcl_error() {
660 let code = r#"
661targetSketch = sketch(on = XY) {
662 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
663 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
664 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
665 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
666 coincident([line1.end, line2.start])
667 coincident([line2.end, line3.start])
668 coincident([line3.end, line4.start])
669 coincident([line4.end, line1.start])
670 equalLength([line1, line2, line3, line4])
671}
672
673target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
674
675tool1Sketch = sketch(on = XY) {
676 line1 = line(start = [var -11, var -11], end = [var -7, var -11])
677 line2 = line(start = [var -7, var -11], end = [var -7, var -7])
678 line3 = line(start = [var -7, var -7], end = [var -11, var -7])
679 line4 = line(start = [var -11, var -7], end = [var -11, var -11])
680 coincident([line1.end, line2.start])
681 coincident([line2.end, line3.start])
682 coincident([line3.end, line4.start])
683 coincident([line4.end, line1.start])
684 equalLength([line1, line2, line3, line4])
685}
686
687tool1 = extrude(region(point = [-9, -9], sketch = tool1Sketch), length = 4)
688
689tool2Sketch = sketch(on = XY) {
690 line1 = line(start = [var 7, var 7], end = [var 11, var 7])
691 line2 = line(start = [var 11, var 7], end = [var 11, var 11])
692 line3 = line(start = [var 11, var 11], end = [var 7, var 11])
693 line4 = line(start = [var 7, var 11], end = [var 7, var 7])
694 coincident([line1.end, line2.start])
695 coincident([line2.end, line3.start])
696 coincident([line3.end, line4.start])
697 coincident([line4.end, line1.start])
698 equalLength([line1, line2, line3, line4])
699}
700
701tool2 = extrude(region(point = [9, 9], sketch = tool2Sketch), length = 4)
702
703first = subtract(target, tools = [tool1])
704second = subtract(target, tools = [tool2])
705"#;
706
707 let ctx = crate::ExecutorContext::new_mock(None).await;
708 let program = crate::Program::parse_no_errs(code).unwrap();
709 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
710 ctx.close().await;
711
712 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
713 let message = err.error.message();
714 assert!(
715 message.contains("`target` was already consumed by a `subtract` operation"),
716 "{message}"
717 );
718 assert!(
719 message.contains("The operation result is now in `first`; use that for subsequent operations"),
720 "{message}"
721 );
722 }
723
724 #[tokio::test(flavor = "multi_thread")]
725 async fn subtract_reusing_consumed_tool_reports_kcl_error() {
726 let code = r#"
727targetSketch = sketch(on = XY) {
728 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
729 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
730 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
731 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
732 coincident([line1.end, line2.start])
733 coincident([line2.end, line3.start])
734 coincident([line3.end, line4.start])
735 coincident([line4.end, line1.start])
736 equalLength([line1, line2, line3, line4])
737}
738
739target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
740
741toolSketch = sketch(on = XY) {
742 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
743 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
744 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
745 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
746 coincident([line1.end, line2.start])
747 coincident([line2.end, line3.start])
748 coincident([line3.end, line4.start])
749 coincident([line4.end, line1.start])
750 equalLength([line1, line2, line3, line4])
751}
752
753tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
754
755first = subtract(target, tools = [tool])
756second = subtract(first, tools = [tool])
757"#;
758
759 let ctx = crate::ExecutorContext::new_mock(None).await;
760 let program = crate::Program::parse_no_errs(code).unwrap();
761 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
762 ctx.close().await;
763
764 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
765 let message = err.error.message();
766 assert!(
767 message.contains("`tool` was already consumed by a `subtract` operation"),
768 "{message}"
769 );
770 assert!(message.contains("can no longer be used"), "{message}");
771 }
772
773 #[tokio::test(flavor = "multi_thread")]
774 async fn union_reusing_consumed_solid_reports_kcl_error() {
775 let code = r#"
776leftSketch = sketch(on = XY) {
777 line1 = line(start = [var -10, var -10], end = [var -2, var -10])
778 line2 = line(start = [var -2, var -10], end = [var -2, var -2])
779 line3 = line(start = [var -2, var -2], end = [var -10, var -2])
780 line4 = line(start = [var -10, var -2], end = [var -10, var -10])
781 coincident([line1.end, line2.start])
782 coincident([line2.end, line3.start])
783 coincident([line3.end, line4.start])
784 coincident([line4.end, line1.start])
785 equalLength([line1, line2, line3, line4])
786}
787
788left = extrude(region(point = [-6, -6], sketch = leftSketch), length = 8)
789
790rightSketch = sketch(on = XY) {
791 line1 = line(start = [var -2, var -2], end = [var 6, var -2])
792 line2 = line(start = [var 6, var -2], end = [var 6, var 6])
793 line3 = line(start = [var 6, var 6], end = [var -2, var 6])
794 line4 = line(start = [var -2, var 6], end = [var -2, var -2])
795 coincident([line1.end, line2.start])
796 coincident([line2.end, line3.start])
797 coincident([line3.end, line4.start])
798 coincident([line4.end, line1.start])
799 equalLength([line1, line2, line3, line4])
800}
801
802right = extrude(region(point = [2, 2], sketch = rightSketch), length = 8)
803
804toolSketch = sketch(on = XY) {
805 line1 = line(start = [var -1, var -1], end = [var 1, var -1])
806 line2 = line(start = [var 1, var -1], end = [var 1, var 1])
807 line3 = line(start = [var 1, var 1], end = [var -1, var 1])
808 line4 = line(start = [var -1, var 1], end = [var -1, var -1])
809 coincident([line1.end, line2.start])
810 coincident([line2.end, line3.start])
811 coincident([line3.end, line4.start])
812 coincident([line4.end, line1.start])
813 equalLength([line1, line2, line3, line4])
814}
815
816tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 2)
817
818first = union([left, right])
819second = union([first, tool])
820third = subtract(left, tools = [tool])
821"#;
822
823 let ctx = crate::ExecutorContext::new_mock(None).await;
824 let program = crate::Program::parse_no_errs(code).unwrap();
825 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
826 ctx.close().await;
827
828 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
829 let message = err.error.message();
830 assert!(
831 message.contains("`left` was already consumed by a `union` operation"),
832 "{message}"
833 );
834 assert!(
835 message.contains("The operation result is now in `second`; use that for subsequent operations"),
836 "{message}"
837 );
838 }
839
840 #[tokio::test(flavor = "multi_thread")]
841 async fn intersect_reusing_consumed_solid_reports_kcl_error() {
842 let code = r#"
843leftSketch = sketch(on = XY) {
844 line1 = line(start = [var -10, var -10], end = [var 4, var -10])
845 line2 = line(start = [var 4, var -10], end = [var 4, var 4])
846 line3 = line(start = [var 4, var 4], end = [var -10, var 4])
847 line4 = line(start = [var -10, var 4], end = [var -10, var -10])
848 coincident([line1.end, line2.start])
849 coincident([line2.end, line3.start])
850 coincident([line3.end, line4.start])
851 coincident([line4.end, line1.start])
852 equalLength([line1, line2, line3, line4])
853}
854
855left = extrude(region(point = [-3, -3], sketch = leftSketch), length = 8)
856
857rightSketch = sketch(on = XY) {
858 line1 = line(start = [var -4, var -4], end = [var 10, var -4])
859 line2 = line(start = [var 10, var -4], end = [var 10, var 10])
860 line3 = line(start = [var 10, var 10], end = [var -4, var 10])
861 line4 = line(start = [var -4, var 10], end = [var -4, var -4])
862 coincident([line1.end, line2.start])
863 coincident([line2.end, line3.start])
864 coincident([line3.end, line4.start])
865 coincident([line4.end, line1.start])
866 equalLength([line1, line2, line3, line4])
867}
868
869right = extrude(region(point = [3, 3], sketch = rightSketch), length = 8)
870
871toolSketch = sketch(on = XY) {
872 line1 = line(start = [var -1, var -1], end = [var 1, var -1])
873 line2 = line(start = [var 1, var -1], end = [var 1, var 1])
874 line3 = line(start = [var 1, var 1], end = [var -1, var 1])
875 line4 = line(start = [var -1, var 1], end = [var -1, var -1])
876 coincident([line1.end, line2.start])
877 coincident([line2.end, line3.start])
878 coincident([line3.end, line4.start])
879 coincident([line4.end, line1.start])
880 equalLength([line1, line2, line3, line4])
881}
882
883tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 2)
884
885first = intersect([left, right])
886second = subtract(left, tools = [tool])
887"#;
888
889 let ctx = crate::ExecutorContext::new_mock(None).await;
890 let program = crate::Program::parse_no_errs(code).unwrap();
891 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
892 ctx.close().await;
893
894 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
895 let message = err.error.message();
896 assert!(
897 message.contains("`left` was already consumed by an `intersect` operation"),
898 "{message}"
899 );
900 assert!(
901 message.contains("The operation result is now in `first`; use that for subsequent operations"),
902 "{message}"
903 );
904 }
905
906 #[tokio::test(flavor = "multi_thread")]
907 async fn split_keep_tools_does_not_consume_tools() {
908 let code = r#"
909targetSketch = sketch(on = XY) {
910 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
911 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
912 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
913 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
914 coincident([line1.end, line2.start])
915 coincident([line2.end, line3.start])
916 coincident([line3.end, line4.start])
917 coincident([line4.end, line1.start])
918 equalLength([line1, line2, line3, line4])
919}
920
921target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
922
923toolSketch = sketch(on = XY) {
924 line1 = line(start = [var -2, var -10], end = [var 2, var -10])
925 line2 = line(start = [var 2, var -10], end = [var 2, var 10])
926 line3 = line(start = [var 2, var 10], end = [var -2, var 10])
927 line4 = line(start = [var -2, var 10], end = [var -2, var -10])
928 coincident([line1.end, line2.start])
929 coincident([line2.end, line3.start])
930 coincident([line3.end, line4.start])
931 coincident([line4.end, line1.start])
932}
933
934tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 20)
935
936first = split(target, tools = [tool], keepTools = true)
937second = subtract(first, tools = [tool])
938"#;
939
940 let ctx = crate::ExecutorContext::new_mock(None).await;
941 let program = crate::Program::parse_no_errs(code).unwrap();
942 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
943 ctx.close().await;
944
945 assert!(outcome.variables.contains_key("second"));
946 }
947
948 #[tokio::test(flavor = "multi_thread")]
949 async fn split_without_keep_tools_consumes_tools() {
950 let code = r#"
951targetSketch = sketch(on = XY) {
952 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
953 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
954 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
955 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
956 coincident([line1.end, line2.start])
957 coincident([line2.end, line3.start])
958 coincident([line3.end, line4.start])
959 coincident([line4.end, line1.start])
960 equalLength([line1, line2, line3, line4])
961}
962
963target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
964
965toolSketch = sketch(on = XY) {
966 line1 = line(start = [var -2, var -10], end = [var 2, var -10])
967 line2 = line(start = [var 2, var -10], end = [var 2, var 10])
968 line3 = line(start = [var 2, var 10], end = [var -2, var 10])
969 line4 = line(start = [var -2, var 10], end = [var -2, var -10])
970 coincident([line1.end, line2.start])
971 coincident([line2.end, line3.start])
972 coincident([line3.end, line4.start])
973 coincident([line4.end, line1.start])
974}
975
976tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 20)
977
978first = split(target, tools = [tool])
979second = subtract(first, tools = [tool])
980"#;
981
982 let ctx = crate::ExecutorContext::new_mock(None).await;
983 let program = crate::Program::parse_no_errs(code).unwrap();
984 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
985 ctx.close().await;
986
987 assert!(matches!(&err.error, KclError::Semantic { .. }), "{:?}", err.error);
988 let message = err.error.message();
989 assert!(
990 message.contains("`tool` was already consumed by a `split` operation"),
991 "{message}"
992 );
993 assert!(message.contains("can no longer be used"), "{message}");
994 }
995}