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