1use anyhow::Result;
2use ezpz::CircleSide;
3use ezpz::Constraint as SolverConstraint;
4use ezpz::LineSide;
5use ezpz::datatypes::AngleKind;
6use ezpz::datatypes::inputs::DatumCircle;
7use ezpz::datatypes::inputs::DatumCircularArc;
8use ezpz::datatypes::inputs::DatumDistance;
9use ezpz::datatypes::inputs::DatumLineSegment;
10use ezpz::datatypes::inputs::DatumPoint;
11use kittycad_modeling_cmds as kcmc;
12
13use crate::errors::KclError;
14use crate::errors::KclErrorDetails;
15use crate::execution::AbstractSegment;
16#[cfg(feature = "artifact-graph")]
17use crate::execution::Artifact;
18#[cfg(feature = "artifact-graph")]
19use crate::execution::CodeRef;
20use crate::execution::ConstrainablePoint2d;
21use crate::execution::ConstrainablePoint2dOrOrigin;
22use crate::execution::ConstraintKey;
23use crate::execution::ConstraintState;
24use crate::execution::ExecState;
25use crate::execution::KclValue;
26use crate::execution::SegmentRepr;
27#[cfg(feature = "artifact-graph")]
28use crate::execution::SketchBlockConstraint;
29#[cfg(feature = "artifact-graph")]
30use crate::execution::SketchBlockConstraintType;
31use crate::execution::SketchConstraint;
32use crate::execution::SketchConstraintKind;
33use crate::execution::SketchVarId;
34use crate::execution::TangencyMode;
35use crate::execution::UnsolvedExpr;
36use crate::execution::UnsolvedSegment;
37use crate::execution::UnsolvedSegmentKind;
38use crate::execution::normalize_to_solver_distance_unit;
39use crate::execution::solver_numeric_type;
40use crate::execution::types::ArrayLen;
41use crate::execution::types::PrimitiveType;
42use crate::execution::types::RuntimeType;
43use crate::front::ArcCtor;
44use crate::front::CircleCtor;
45#[cfg(feature = "artifact-graph")]
46use crate::front::Coincident;
47#[cfg(feature = "artifact-graph")]
48use crate::front::Constraint;
49#[cfg(feature = "artifact-graph")]
50use crate::front::EqualRadius;
51#[cfg(feature = "artifact-graph")]
52use crate::front::Horizontal;
53use crate::front::LineCtor;
54#[cfg(feature = "artifact-graph")]
55use crate::front::LinesEqualLength;
56#[cfg(feature = "artifact-graph")]
57use crate::front::Midpoint;
58use crate::front::Number;
59#[cfg(feature = "artifact-graph")]
60use crate::front::Object;
61use crate::front::ObjectId;
62#[cfg(feature = "artifact-graph")]
63use crate::front::ObjectKind;
64#[cfg(feature = "artifact-graph")]
65use crate::front::Parallel;
66#[cfg(feature = "artifact-graph")]
67use crate::front::Perpendicular;
68use crate::front::Point2d;
69use crate::front::PointCtor;
70#[cfg(feature = "artifact-graph")]
71use crate::front::SourceRef;
72#[cfg(feature = "artifact-graph")]
73use crate::front::Symmetric;
74#[cfg(feature = "artifact-graph")]
75use crate::front::Tangent;
76#[cfg(feature = "artifact-graph")]
77use crate::front::Vertical;
78#[cfg(feature = "artifact-graph")]
79use crate::frontend::sketch::ConstraintSegment;
80use crate::std::Args;
81use crate::std::args::FromKclValue;
82use crate::std::args::TyF64;
83
84fn point2d_is_origin(point2d: &KclValue) -> bool {
85 let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point2d) else {
86 return false;
87 };
88 if x.ty.as_length().is_none() || y.ty.as_length().is_none() {
91 return false;
92 }
93 x.n == 0.0 && y.n == 0.0
96}
97
98#[derive(Debug, Clone, Copy)]
99struct LineVars {
100 start: [SketchVarId; 2],
101 end: [SketchVarId; 2],
102}
103
104#[derive(Debug, Clone, Copy)]
105struct ArcVars {
106 center: [SketchVarId; 2],
107 start: [SketchVarId; 2],
108 end: Option<[SketchVarId; 2]>,
109}
110
111fn make_line_arc_tangency_key(line: LineVars, arc: ArcVars) -> ConstraintKey {
112 let [a0, a1, a2, a3] = flatten_line_vars(line);
113 let [b0, b1, b2, b3, b4, b5] = flatten_arc_vars(arc);
114 ConstraintKey::LineCircle([a0, a1, a2, a3, b0, b1, b2, b3, b4, b5])
115}
116
117fn make_arc_arc_tangency_key(arc_a: ArcVars, arc_b: ArcVars) -> ConstraintKey {
118 let flat_a = flatten_arc_vars(arc_a);
119 let flat_b = flatten_arc_vars(arc_b);
120 let (lhs, rhs) = if flat_a <= flat_b {
121 (flat_a, flat_b)
122 } else {
123 (flat_b, flat_a)
124 };
125 let [a0, a1, a2, a3, a4, a5] = lhs;
126 let [b0, b1, b2, b3, b4, b5] = rhs;
127 ConstraintKey::CircleCircle([a0, a1, a2, a3, a4, a5, b0, b1, b2, b3, b4, b5])
128}
129
130fn flatten_line_vars(line: LineVars) -> [usize; 4] {
131 [line.start[0].0, line.start[1].0, line.end[0].0, line.end[1].0]
132}
133
134fn flatten_arc_vars(arc: ArcVars) -> [usize; 6] {
135 let end = arc.end.unwrap_or([SketchVarId::INVALID; 2]);
136 [
137 arc.center[0].0,
138 arc.center[1].0,
139 arc.start[0].0,
140 arc.start[1].0,
141 end[0].0,
142 end[1].0,
143 ]
144}
145
146fn infer_line_tangent_side(
147 sketch_vars: &[KclValue],
148 line: LineVars,
149 circle_center: [SketchVarId; 2],
150 exec_state: &mut ExecState,
151 range: crate::SourceRange,
152) -> Result<LineSide, KclError> {
153 let [sx, sy] = point_initial_position(sketch_vars, line.start, exec_state, range)?;
154 let [ex, ey] = point_initial_position(sketch_vars, line.end, exec_state, range)?;
155 let [cx, cy] = point_initial_position(sketch_vars, circle_center, exec_state, range)?;
156 let cross = (ex - sx) * (cy - sy) - (ey - sy) * (cx - sx);
157 Ok(if cross >= 0.0 { LineSide::Left } else { LineSide::Right })
158}
159
160fn infer_arc_tangent_side(
161 sketch_vars: &[KclValue],
162 arc_a: ArcVars,
163 arc_b: ArcVars,
164 exec_state: &mut ExecState,
165 range: crate::SourceRange,
166) -> Result<CircleSide, KclError> {
167 let rad_a = arc_initial_radius(sketch_vars, arc_a, exec_state, range)?;
168 let rad_b = arc_initial_radius(sketch_vars, arc_b, exec_state, range)?;
169 infer_circle_tangent_side(sketch_vars, arc_a.center, arc_b.center, rad_a, rad_b, exec_state, range)
170}
171
172fn infer_circle_tangent_side(
173 sketch_vars: &[KclValue],
174 center_a: [SketchVarId; 2],
175 center_b: [SketchVarId; 2],
176 radius_a: f64,
177 radius_b: f64,
178 exec_state: &mut ExecState,
179 range: crate::SourceRange,
180) -> Result<CircleSide, KclError> {
181 let dist = points_initial_distance(sketch_vars, center_a, center_b, exec_state, range)?;
182 let r_int = ((radius_a - radius_b).abs() - dist).abs();
183 let r_ext = (radius_a + radius_b - dist).abs();
184 Ok(if r_int < r_ext {
185 CircleSide::Interior
186 } else {
187 CircleSide::Exterior
188 })
189}
190
191fn point_initial_position(
192 sketch_vars: &[KclValue],
193 point: [SketchVarId; 2],
194 exec_state: &mut ExecState,
195 range: crate::SourceRange,
196) -> Result<[f64; 2], KclError> {
197 Ok([
198 sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
199 sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
200 ])
201}
202
203fn points_initial_distance(
204 sketch_vars: &[KclValue],
205 point_a: [SketchVarId; 2],
206 point_b: [SketchVarId; 2],
207 exec_state: &mut ExecState,
208 range: crate::SourceRange,
209) -> Result<f64, KclError> {
210 let [a_x, a_y] = point_initial_position(sketch_vars, point_a, exec_state, range)?;
211 let [b_x, b_y] = point_initial_position(sketch_vars, point_b, exec_state, range)?;
212 Ok(libm::hypot(a_x - b_x, a_y - b_y))
213}
214
215fn arc_initial_radius(
216 sketch_vars: &[KclValue],
217 arc: ArcVars,
218 exec_state: &mut ExecState,
219 range: crate::SourceRange,
220) -> Result<f64, KclError> {
221 points_initial_distance(sketch_vars, arc.center, arc.start, exec_state, range)
222}
223
224fn extract_arc_component(
231 value: &KclValue,
232 exec_state: &mut ExecState,
233 range: crate::SourceRange,
234 description: &str,
235) -> Result<(SketchVarId, Option<SolverConstraint>), KclError> {
236 match value.as_unsolved_expr() {
237 None => Err(KclError::new_semantic(KclErrorDetails::new(
238 format!("{description} must be a number or sketch var"),
239 vec![range],
240 ))),
241 Some(UnsolvedExpr::Unknown(var_id)) => Ok((var_id, None)),
242 Some(UnsolvedExpr::Known(_)) => {
243 let value_in_solver_units = normalize_to_solver_distance_unit(value, range, exec_state, description)?;
244 let Some(normalized_value) = value_in_solver_units.as_ty_f64() else {
245 return Err(KclError::new_internal(KclErrorDetails::new(
246 "Expected number after coercion".to_owned(),
247 vec![range],
248 )));
249 };
250
251 let Some(sketch_state) = exec_state.sketch_block_mut() else {
252 return Err(KclError::new_semantic(KclErrorDetails::new(
253 "arc() can only be used inside a sketch block".to_owned(),
254 vec![range],
255 )));
256 };
257 let var_id = sketch_state.next_sketch_var_id();
258 sketch_state.sketch_vars.push(KclValue::SketchVar {
259 value: Box::new(crate::execution::SketchVar {
260 id: var_id,
261 initial_value: normalized_value.n,
262 ty: normalized_value.ty,
263 meta: vec![],
264 }),
265 });
266
267 Ok((
268 var_id,
269 Some(SolverConstraint::Fixed(
270 var_id.to_constraint_id(range)?,
271 normalized_value.n,
272 )),
273 ))
274 }
275 }
276}
277
278#[cfg(feature = "artifact-graph")]
279fn coincident_segments_for_segment_and_point2d(
280 segment_id: ObjectId,
281 point2d: &KclValue,
282 segment_first: bool,
283) -> Vec<ConstraintSegment> {
284 if !point2d_is_origin(point2d) {
285 return vec![segment_id.into()];
286 }
287
288 if segment_first {
289 vec![segment_id.into(), ConstraintSegment::ORIGIN]
290 } else {
291 vec![ConstraintSegment::ORIGIN, segment_id.into()]
292 }
293}
294
295pub async fn point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
296 let at: Vec<KclValue> = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
297 let [at_x_value, at_y_value]: [KclValue; 2] = at.try_into().map_err(|_| {
298 KclError::new_semantic(KclErrorDetails::new(
299 "at must be a 2D point".to_owned(),
300 vec![args.source_range],
301 ))
302 })?;
303 let Some(at_x) = at_x_value.as_unsolved_expr() else {
304 return Err(KclError::new_semantic(KclErrorDetails::new(
305 "at x must be a number or sketch var".to_owned(),
306 vec![args.source_range],
307 )));
308 };
309 let Some(at_y) = at_y_value.as_unsolved_expr() else {
310 return Err(KclError::new_semantic(KclErrorDetails::new(
311 "at y must be a number or sketch var".to_owned(),
312 vec![args.source_range],
313 )));
314 };
315 let ctor = PointCtor {
316 position: Point2d {
317 x: at_x_value.to_sketch_expr().ok_or_else(|| {
318 KclError::new_semantic(KclErrorDetails::new(
319 "unable to convert numeric type to suffix".to_owned(),
320 vec![args.source_range],
321 ))
322 })?,
323 y: at_y_value.to_sketch_expr().ok_or_else(|| {
324 KclError::new_semantic(KclErrorDetails::new(
325 "unable to convert numeric type to suffix".to_owned(),
326 vec![args.source_range],
327 ))
328 })?,
329 },
330 };
331 let segment = UnsolvedSegment {
332 id: exec_state.next_uuid(),
333 object_id: exec_state.next_object_id(),
334 kind: UnsolvedSegmentKind::Point {
335 position: [at_x, at_y],
336 ctor: Box::new(ctor),
337 },
338 tag: None,
339 node_path: args.node_path.clone(),
340 meta: vec![args.source_range.into()],
341 };
342 #[cfg(feature = "artifact-graph")]
343 let optional_constraints = {
344 let object_id = exec_state.add_placeholder_scene_object(segment.object_id, args.source_range, args.node_path);
345
346 let mut optional_constraints = Vec::new();
347 if exec_state.segment_ids_edited_contains(&object_id) {
348 if let Some(at_x_var) = at_x_value.as_sketch_var() {
349 let x_initial_value = at_x_var.initial_value_to_solver_units(
350 exec_state,
351 args.source_range,
352 "edited segment fixed constraint value",
353 )?;
354 optional_constraints.push(SolverConstraint::Fixed(
355 at_x_var.id.to_constraint_id(args.source_range)?,
356 x_initial_value.n,
357 ));
358 }
359 if let Some(at_y_var) = at_y_value.as_sketch_var() {
360 let y_initial_value = at_y_var.initial_value_to_solver_units(
361 exec_state,
362 args.source_range,
363 "edited segment fixed constraint value",
364 )?;
365 optional_constraints.push(SolverConstraint::Fixed(
366 at_y_var.id.to_constraint_id(args.source_range)?,
367 y_initial_value.n,
368 ));
369 }
370 }
371 optional_constraints
372 };
373
374 let Some(sketch_state) = exec_state.sketch_block_mut() else {
376 return Err(KclError::new_semantic(KclErrorDetails::new(
377 "line() can only be used inside a sketch block".to_owned(),
378 vec![args.source_range],
379 )));
380 };
381 sketch_state.needed_by_engine.push(segment.clone());
382
383 #[cfg(feature = "artifact-graph")]
384 sketch_state.solver_optional_constraints.extend(optional_constraints);
385
386 let meta = segment.meta.clone();
387 let abstract_segment = AbstractSegment {
388 repr: SegmentRepr::Unsolved {
389 segment: Box::new(segment),
390 },
391 meta,
392 };
393 Ok(KclValue::Segment {
394 value: Box::new(abstract_segment),
395 })
396}
397
398pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
399 let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
400 let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
401 let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
402 let construction: bool = construction_opt.unwrap_or(false);
403 let construction_ctor = construction_opt;
404 let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
405 KclError::new_semantic(KclErrorDetails::new(
406 "start must be a 2D point".to_owned(),
407 vec![args.source_range],
408 ))
409 })?;
410 let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
411 KclError::new_semantic(KclErrorDetails::new(
412 "end must be a 2D point".to_owned(),
413 vec![args.source_range],
414 ))
415 })?;
416 let Some(start_x) = start_x_value.as_unsolved_expr() else {
417 return Err(KclError::new_semantic(KclErrorDetails::new(
418 "start x must be a number or sketch var".to_owned(),
419 vec![args.source_range],
420 )));
421 };
422 let Some(start_y) = start_y_value.as_unsolved_expr() else {
423 return Err(KclError::new_semantic(KclErrorDetails::new(
424 "start y must be a number or sketch var".to_owned(),
425 vec![args.source_range],
426 )));
427 };
428 let Some(end_x) = end_x_value.as_unsolved_expr() else {
429 return Err(KclError::new_semantic(KclErrorDetails::new(
430 "end x must be a number or sketch var".to_owned(),
431 vec![args.source_range],
432 )));
433 };
434 let Some(end_y) = end_y_value.as_unsolved_expr() else {
435 return Err(KclError::new_semantic(KclErrorDetails::new(
436 "end y must be a number or sketch var".to_owned(),
437 vec![args.source_range],
438 )));
439 };
440 let ctor = LineCtor {
441 start: Point2d {
442 x: start_x_value.to_sketch_expr().ok_or_else(|| {
443 KclError::new_semantic(KclErrorDetails::new(
444 "unable to convert numeric type to suffix".to_owned(),
445 vec![args.source_range],
446 ))
447 })?,
448 y: start_y_value.to_sketch_expr().ok_or_else(|| {
449 KclError::new_semantic(KclErrorDetails::new(
450 "unable to convert numeric type to suffix".to_owned(),
451 vec![args.source_range],
452 ))
453 })?,
454 },
455 end: Point2d {
456 x: end_x_value.to_sketch_expr().ok_or_else(|| {
457 KclError::new_semantic(KclErrorDetails::new(
458 "unable to convert numeric type to suffix".to_owned(),
459 vec![args.source_range],
460 ))
461 })?,
462 y: end_y_value.to_sketch_expr().ok_or_else(|| {
463 KclError::new_semantic(KclErrorDetails::new(
464 "unable to convert numeric type to suffix".to_owned(),
465 vec![args.source_range],
466 ))
467 })?,
468 },
469 construction: construction_ctor,
470 };
471 let start_object_id = exec_state.next_object_id();
473 let end_object_id = exec_state.next_object_id();
474 let line_object_id = exec_state.next_object_id();
475 let segment = UnsolvedSegment {
476 id: exec_state.next_uuid(),
477 object_id: line_object_id,
478 kind: UnsolvedSegmentKind::Line {
479 start: [start_x, start_y],
480 end: [end_x, end_y],
481 ctor: Box::new(ctor),
482 start_object_id,
483 end_object_id,
484 construction,
485 },
486 tag: None,
487 node_path: args.node_path.clone(),
488 meta: vec![args.source_range.into()],
489 };
490 #[cfg(feature = "artifact-graph")]
491 let optional_constraints = {
492 let start_object_id =
493 exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
494 let end_object_id =
495 exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
496 let line_object_id =
497 exec_state.add_placeholder_scene_object(line_object_id, args.source_range, args.node_path.clone());
498
499 let mut optional_constraints = Vec::new();
500 if exec_state.segment_ids_edited_contains(&start_object_id)
501 || exec_state.segment_ids_edited_contains(&line_object_id)
502 {
503 if let Some(start_x_var) = start_x_value.as_sketch_var() {
504 let x_initial_value = start_x_var.initial_value_to_solver_units(
505 exec_state,
506 args.source_range,
507 "edited segment fixed constraint value",
508 )?;
509 optional_constraints.push(SolverConstraint::Fixed(
510 start_x_var.id.to_constraint_id(args.source_range)?,
511 x_initial_value.n,
512 ));
513 }
514 if let Some(start_y_var) = start_y_value.as_sketch_var() {
515 let y_initial_value = start_y_var.initial_value_to_solver_units(
516 exec_state,
517 args.source_range,
518 "edited segment fixed constraint value",
519 )?;
520 optional_constraints.push(SolverConstraint::Fixed(
521 start_y_var.id.to_constraint_id(args.source_range)?,
522 y_initial_value.n,
523 ));
524 }
525 }
526 if exec_state.segment_ids_edited_contains(&end_object_id)
527 || exec_state.segment_ids_edited_contains(&line_object_id)
528 {
529 if let Some(end_x_var) = end_x_value.as_sketch_var() {
530 let x_initial_value = end_x_var.initial_value_to_solver_units(
531 exec_state,
532 args.source_range,
533 "edited segment fixed constraint value",
534 )?;
535 optional_constraints.push(SolverConstraint::Fixed(
536 end_x_var.id.to_constraint_id(args.source_range)?,
537 x_initial_value.n,
538 ));
539 }
540 if let Some(end_y_var) = end_y_value.as_sketch_var() {
541 let y_initial_value = end_y_var.initial_value_to_solver_units(
542 exec_state,
543 args.source_range,
544 "edited segment fixed constraint value",
545 )?;
546 optional_constraints.push(SolverConstraint::Fixed(
547 end_y_var.id.to_constraint_id(args.source_range)?,
548 y_initial_value.n,
549 ));
550 }
551 }
552 optional_constraints
553 };
554
555 let Some(sketch_state) = exec_state.sketch_block_mut() else {
557 return Err(KclError::new_semantic(KclErrorDetails::new(
558 "line() can only be used inside a sketch block".to_owned(),
559 vec![args.source_range],
560 )));
561 };
562 sketch_state.needed_by_engine.push(segment.clone());
563
564 #[cfg(feature = "artifact-graph")]
565 sketch_state.solver_optional_constraints.extend(optional_constraints);
566
567 let meta = segment.meta.clone();
568 let abstract_segment = AbstractSegment {
569 repr: SegmentRepr::Unsolved {
570 segment: Box::new(segment),
571 },
572 meta,
573 };
574 Ok(KclValue::Segment {
575 value: Box::new(abstract_segment),
576 })
577}
578
579pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
580 let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
581 let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
582 let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
584 let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
585 let construction: bool = construction_opt.unwrap_or(false);
586 let construction_ctor = construction_opt;
587
588 let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
589 KclError::new_semantic(KclErrorDetails::new(
590 "start must be a 2D point".to_owned(),
591 vec![args.source_range],
592 ))
593 })?;
594 let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
595 KclError::new_semantic(KclErrorDetails::new(
596 "end must be a 2D point".to_owned(),
597 vec![args.source_range],
598 ))
599 })?;
600 let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
601 KclError::new_semantic(KclErrorDetails::new(
602 "center must be a 2D point".to_owned(),
603 vec![args.source_range],
604 ))
605 })?;
606
607 let (start_x, start_x_fixed) = extract_arc_component(&start_x_value, exec_state, args.source_range, "start x")?;
608 let (start_y, start_y_fixed) = extract_arc_component(&start_y_value, exec_state, args.source_range, "start y")?;
609 let (end_x, end_x_fixed) = extract_arc_component(&end_x_value, exec_state, args.source_range, "end x")?;
610 let (end_y, end_y_fixed) = extract_arc_component(&end_y_value, exec_state, args.source_range, "end y")?;
611 let (center_x, center_x_fixed) = extract_arc_component(¢er_x_value, exec_state, args.source_range, "center x")?;
612 let (center_y, center_y_fixed) = extract_arc_component(¢er_y_value, exec_state, args.source_range, "center y")?;
613 let arc_fixed_constraints = [
616 start_x_fixed,
617 start_y_fixed,
618 end_x_fixed,
619 end_y_fixed,
620 center_x_fixed,
621 center_y_fixed,
622 ]
623 .into_iter()
624 .flatten();
625
626 let ctor = ArcCtor {
627 start: Point2d {
628 x: start_x_value.to_sketch_expr().ok_or_else(|| {
629 KclError::new_semantic(KclErrorDetails::new(
630 "unable to convert numeric type to suffix".to_owned(),
631 vec![args.source_range],
632 ))
633 })?,
634 y: start_y_value.to_sketch_expr().ok_or_else(|| {
635 KclError::new_semantic(KclErrorDetails::new(
636 "unable to convert numeric type to suffix".to_owned(),
637 vec![args.source_range],
638 ))
639 })?,
640 },
641 end: Point2d {
642 x: end_x_value.to_sketch_expr().ok_or_else(|| {
643 KclError::new_semantic(KclErrorDetails::new(
644 "unable to convert numeric type to suffix".to_owned(),
645 vec![args.source_range],
646 ))
647 })?,
648 y: end_y_value.to_sketch_expr().ok_or_else(|| {
649 KclError::new_semantic(KclErrorDetails::new(
650 "unable to convert numeric type to suffix".to_owned(),
651 vec![args.source_range],
652 ))
653 })?,
654 },
655 center: Point2d {
656 x: center_x_value.to_sketch_expr().ok_or_else(|| {
657 KclError::new_semantic(KclErrorDetails::new(
658 "unable to convert numeric type to suffix".to_owned(),
659 vec![args.source_range],
660 ))
661 })?,
662 y: center_y_value.to_sketch_expr().ok_or_else(|| {
663 KclError::new_semantic(KclErrorDetails::new(
664 "unable to convert numeric type to suffix".to_owned(),
665 vec![args.source_range],
666 ))
667 })?,
668 },
669 construction: construction_ctor,
670 };
671
672 let start_object_id = exec_state.next_object_id();
674 let end_object_id = exec_state.next_object_id();
675 let center_object_id = exec_state.next_object_id();
676 let arc_object_id = exec_state.next_object_id();
677 let segment = UnsolvedSegment {
678 id: exec_state.next_uuid(),
679 object_id: arc_object_id,
680 kind: UnsolvedSegmentKind::Arc {
681 start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
682 end: [UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)],
683 center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
684 ctor: Box::new(ctor),
685 start_object_id,
686 end_object_id,
687 center_object_id,
688 construction,
689 },
690 tag: None,
691 node_path: args.node_path.clone(),
692 meta: vec![args.source_range.into()],
693 };
694 #[cfg(feature = "artifact-graph")]
695 let optional_constraints = {
696 let start_object_id =
697 exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
698 let end_object_id =
699 exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
700 let center_object_id =
701 exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
702 let arc_object_id =
703 exec_state.add_placeholder_scene_object(arc_object_id, args.source_range, args.node_path.clone());
704
705 let mut optional_constraints = Vec::new();
706 if exec_state.segment_ids_edited_contains(&start_object_id)
707 || exec_state.segment_ids_edited_contains(&arc_object_id)
708 {
709 if let Some(start_x_var) = start_x_value.as_sketch_var() {
710 let x_initial_value = start_x_var.initial_value_to_solver_units(
711 exec_state,
712 args.source_range,
713 "edited segment fixed constraint value",
714 )?;
715 optional_constraints.push(ezpz::Constraint::Fixed(
716 start_x_var.id.to_constraint_id(args.source_range)?,
717 x_initial_value.n,
718 ));
719 }
720 if let Some(start_y_var) = start_y_value.as_sketch_var() {
721 let y_initial_value = start_y_var.initial_value_to_solver_units(
722 exec_state,
723 args.source_range,
724 "edited segment fixed constraint value",
725 )?;
726 optional_constraints.push(ezpz::Constraint::Fixed(
727 start_y_var.id.to_constraint_id(args.source_range)?,
728 y_initial_value.n,
729 ));
730 }
731 }
732 if exec_state.segment_ids_edited_contains(&end_object_id)
733 || exec_state.segment_ids_edited_contains(&arc_object_id)
734 {
735 if let Some(end_x_var) = end_x_value.as_sketch_var() {
736 let x_initial_value = end_x_var.initial_value_to_solver_units(
737 exec_state,
738 args.source_range,
739 "edited segment fixed constraint value",
740 )?;
741 optional_constraints.push(ezpz::Constraint::Fixed(
742 end_x_var.id.to_constraint_id(args.source_range)?,
743 x_initial_value.n,
744 ));
745 }
746 if let Some(end_y_var) = end_y_value.as_sketch_var() {
747 let y_initial_value = end_y_var.initial_value_to_solver_units(
748 exec_state,
749 args.source_range,
750 "edited segment fixed constraint value",
751 )?;
752 optional_constraints.push(ezpz::Constraint::Fixed(
753 end_y_var.id.to_constraint_id(args.source_range)?,
754 y_initial_value.n,
755 ));
756 }
757 }
758 if exec_state.segment_ids_edited_contains(¢er_object_id)
759 || exec_state.segment_ids_edited_contains(&arc_object_id)
760 {
761 if let Some(center_x_var) = center_x_value.as_sketch_var() {
762 let x_initial_value = center_x_var.initial_value_to_solver_units(
763 exec_state,
764 args.source_range,
765 "edited segment fixed constraint value",
766 )?;
767 optional_constraints.push(ezpz::Constraint::Fixed(
768 center_x_var.id.to_constraint_id(args.source_range)?,
769 x_initial_value.n,
770 ));
771 }
772 if let Some(center_y_var) = center_y_value.as_sketch_var() {
773 let y_initial_value = center_y_var.initial_value_to_solver_units(
774 exec_state,
775 args.source_range,
776 "edited segment fixed constraint value",
777 )?;
778 optional_constraints.push(ezpz::Constraint::Fixed(
779 center_y_var.id.to_constraint_id(args.source_range)?,
780 y_initial_value.n,
781 ));
782 }
783 }
784 optional_constraints
785 };
786
787 let range = args.source_range;
789 let mut required_constraints = Vec::with_capacity(7);
790 required_constraints.extend(arc_fixed_constraints);
791 required_constraints.push(ezpz::Constraint::Arc(ezpz::datatypes::inputs::DatumCircularArc {
792 center: ezpz::datatypes::inputs::DatumPoint::new_xy(
793 center_x.to_constraint_id(range)?,
794 center_y.to_constraint_id(range)?,
795 ),
796 start: ezpz::datatypes::inputs::DatumPoint::new_xy(
797 start_x.to_constraint_id(range)?,
798 start_y.to_constraint_id(range)?,
799 ),
800 end: ezpz::datatypes::inputs::DatumPoint::new_xy(
801 end_x.to_constraint_id(range)?,
802 end_y.to_constraint_id(range)?,
803 ),
804 }));
805
806 let Some(sketch_state) = exec_state.sketch_block_mut() else {
807 return Err(KclError::new_semantic(KclErrorDetails::new(
808 "arc() can only be used inside a sketch block".to_owned(),
809 vec![args.source_range],
810 )));
811 };
812 sketch_state.needed_by_engine.push(segment.clone());
814 sketch_state.solver_constraints.extend(required_constraints);
816 #[cfg(feature = "artifact-graph")]
820 sketch_state.solver_optional_constraints.extend(optional_constraints);
821
822 let meta = segment.meta.clone();
823 let abstract_segment = AbstractSegment {
824 repr: SegmentRepr::Unsolved {
825 segment: Box::new(segment),
826 },
827 meta,
828 };
829 Ok(KclValue::Segment {
830 value: Box::new(abstract_segment),
831 })
832}
833
834pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
835 let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
836 let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
837 let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
838 let construction: bool = construction_opt.unwrap_or(false);
839 let construction_ctor = construction_opt;
840
841 let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
842 KclError::new_semantic(KclErrorDetails::new(
843 "start must be a 2D point".to_owned(),
844 vec![args.source_range],
845 ))
846 })?;
847 let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
848 KclError::new_semantic(KclErrorDetails::new(
849 "center must be a 2D point".to_owned(),
850 vec![args.source_range],
851 ))
852 })?;
853
854 let Some(UnsolvedExpr::Unknown(start_x)) = start_x_value.as_unsolved_expr() else {
855 return Err(KclError::new_semantic(KclErrorDetails::new(
856 "start x must be a sketch var".to_owned(),
857 vec![args.source_range],
858 )));
859 };
860 let Some(UnsolvedExpr::Unknown(start_y)) = start_y_value.as_unsolved_expr() else {
861 return Err(KclError::new_semantic(KclErrorDetails::new(
862 "start y must be a sketch var".to_owned(),
863 vec![args.source_range],
864 )));
865 };
866 let Some(UnsolvedExpr::Unknown(center_x)) = center_x_value.as_unsolved_expr() else {
867 return Err(KclError::new_semantic(KclErrorDetails::new(
868 "center x must be a sketch var".to_owned(),
869 vec![args.source_range],
870 )));
871 };
872 let Some(UnsolvedExpr::Unknown(center_y)) = center_y_value.as_unsolved_expr() else {
873 return Err(KclError::new_semantic(KclErrorDetails::new(
874 "center y must be a sketch var".to_owned(),
875 vec![args.source_range],
876 )));
877 };
878
879 let ctor = CircleCtor {
880 start: Point2d {
881 x: start_x_value.to_sketch_expr().ok_or_else(|| {
882 KclError::new_semantic(KclErrorDetails::new(
883 "unable to convert numeric type to suffix".to_owned(),
884 vec![args.source_range],
885 ))
886 })?,
887 y: start_y_value.to_sketch_expr().ok_or_else(|| {
888 KclError::new_semantic(KclErrorDetails::new(
889 "unable to convert numeric type to suffix".to_owned(),
890 vec![args.source_range],
891 ))
892 })?,
893 },
894 center: Point2d {
895 x: center_x_value.to_sketch_expr().ok_or_else(|| {
896 KclError::new_semantic(KclErrorDetails::new(
897 "unable to convert numeric type to suffix".to_owned(),
898 vec![args.source_range],
899 ))
900 })?,
901 y: center_y_value.to_sketch_expr().ok_or_else(|| {
902 KclError::new_semantic(KclErrorDetails::new(
903 "unable to convert numeric type to suffix".to_owned(),
904 vec![args.source_range],
905 ))
906 })?,
907 },
908 construction: construction_ctor,
909 };
910
911 let start_object_id = exec_state.next_object_id();
913 let center_object_id = exec_state.next_object_id();
914 let circle_object_id = exec_state.next_object_id();
915 let segment = UnsolvedSegment {
916 id: exec_state.next_uuid(),
917 object_id: circle_object_id,
918 kind: UnsolvedSegmentKind::Circle {
919 start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
920 center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
921 ctor: Box::new(ctor),
922 start_object_id,
923 center_object_id,
924 construction,
925 },
926 tag: None,
927 node_path: args.node_path.clone(),
928 meta: vec![args.source_range.into()],
929 };
930 #[cfg(feature = "artifact-graph")]
931 let optional_constraints = {
932 let start_object_id =
933 exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
934 let center_object_id =
935 exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
936 let circle_object_id =
937 exec_state.add_placeholder_scene_object(circle_object_id, args.source_range, args.node_path.clone());
938
939 let mut optional_constraints = Vec::new();
940 if exec_state.segment_ids_edited_contains(&start_object_id)
941 || exec_state.segment_ids_edited_contains(&circle_object_id)
942 {
943 if let Some(start_x_var) = start_x_value.as_sketch_var() {
944 let x_initial_value = start_x_var.initial_value_to_solver_units(
945 exec_state,
946 args.source_range,
947 "edited segment fixed constraint value",
948 )?;
949 optional_constraints.push(ezpz::Constraint::Fixed(
950 start_x_var.id.to_constraint_id(args.source_range)?,
951 x_initial_value.n,
952 ));
953 }
954 if let Some(start_y_var) = start_y_value.as_sketch_var() {
955 let y_initial_value = start_y_var.initial_value_to_solver_units(
956 exec_state,
957 args.source_range,
958 "edited segment fixed constraint value",
959 )?;
960 optional_constraints.push(ezpz::Constraint::Fixed(
961 start_y_var.id.to_constraint_id(args.source_range)?,
962 y_initial_value.n,
963 ));
964 }
965 }
966 if exec_state.segment_ids_edited_contains(¢er_object_id)
967 || exec_state.segment_ids_edited_contains(&circle_object_id)
968 {
969 if let Some(center_x_var) = center_x_value.as_sketch_var() {
970 let x_initial_value = center_x_var.initial_value_to_solver_units(
971 exec_state,
972 args.source_range,
973 "edited segment fixed constraint value",
974 )?;
975 optional_constraints.push(ezpz::Constraint::Fixed(
976 center_x_var.id.to_constraint_id(args.source_range)?,
977 x_initial_value.n,
978 ));
979 }
980 if let Some(center_y_var) = center_y_value.as_sketch_var() {
981 let y_initial_value = center_y_var.initial_value_to_solver_units(
982 exec_state,
983 args.source_range,
984 "edited segment fixed constraint value",
985 )?;
986 optional_constraints.push(ezpz::Constraint::Fixed(
987 center_y_var.id.to_constraint_id(args.source_range)?,
988 y_initial_value.n,
989 ));
990 }
991 }
992 optional_constraints
993 };
994
995 let Some(sketch_state) = exec_state.sketch_block_mut() else {
996 return Err(KclError::new_semantic(KclErrorDetails::new(
997 "circle() can only be used inside a sketch block".to_owned(),
998 vec![args.source_range],
999 )));
1000 };
1001 sketch_state.needed_by_engine.push(segment.clone());
1003
1004 #[cfg(feature = "artifact-graph")]
1005 sketch_state.solver_optional_constraints.extend(optional_constraints);
1006
1007 let meta = segment.meta.clone();
1008 let abstract_segment = AbstractSegment {
1009 repr: SegmentRepr::Unsolved {
1010 segment: Box::new(segment),
1011 },
1012 meta,
1013 };
1014 Ok(KclValue::Segment {
1015 value: Box::new(abstract_segment),
1016 })
1017}
1018
1019pub async fn coincident(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1020 let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1021 "points",
1022 &RuntimeType::Array(
1023 Box::new(RuntimeType::Union(vec![RuntimeType::segment(), RuntimeType::point2d()])),
1024 ArrayLen::Minimum(2),
1025 ),
1026 exec_state,
1027 )?;
1028 if points.len() > 2 {
1029 return coincident_points(points, exec_state, args);
1030 }
1031 let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
1032 KclError::new_semantic(KclErrorDetails::new(
1033 "must have two input points".to_owned(),
1034 vec![args.source_range],
1035 ))
1036 })?;
1037
1038 let range = args.source_range;
1039 match (&point0, &point1) {
1040 (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1041 let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1042 return Err(KclError::new_semantic(KclErrorDetails::new(
1043 "first point must be an unsolved segment".to_owned(),
1044 vec![args.source_range],
1045 )));
1046 };
1047 let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1048 return Err(KclError::new_semantic(KclErrorDetails::new(
1049 "second point must be an unsolved segment".to_owned(),
1050 vec![args.source_range],
1051 )));
1052 };
1053 match (&unsolved0.kind, &unsolved1.kind) {
1054 (
1055 UnsolvedSegmentKind::Point { position: pos0, .. },
1056 UnsolvedSegmentKind::Point { position: pos1, .. },
1057 ) => {
1058 let p0_x = &pos0[0];
1059 let p0_y = &pos0[1];
1060 match (p0_x, p0_y) {
1061 (UnsolvedExpr::Unknown(p0_x), UnsolvedExpr::Unknown(p0_y)) => {
1062 let p1_x = &pos1[0];
1063 let p1_y = &pos1[1];
1064 match (p1_x, p1_y) {
1065 (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1066 let constraint = SolverConstraint::PointsCoincident(
1067 ezpz::datatypes::inputs::DatumPoint::new_xy(
1068 p0_x.to_constraint_id(range)?,
1069 p0_y.to_constraint_id(range)?,
1070 ),
1071 ezpz::datatypes::inputs::DatumPoint::new_xy(
1072 p1_x.to_constraint_id(range)?,
1073 p1_y.to_constraint_id(range)?,
1074 ),
1075 );
1076 #[cfg(feature = "artifact-graph")]
1077 let constraint_id = exec_state.next_object_id();
1078 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1080 return Err(KclError::new_semantic(KclErrorDetails::new(
1081 "coincident() can only be used inside a sketch block".to_owned(),
1082 vec![args.source_range],
1083 )));
1084 };
1085 sketch_state.solver_constraints.push(constraint);
1086 #[cfg(feature = "artifact-graph")]
1087 {
1088 let constraint = crate::front::Constraint::Coincident(Coincident {
1089 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1090 });
1091 sketch_state.sketch_constraints.push(constraint_id);
1092 track_constraint(constraint_id, constraint, exec_state, &args);
1093 }
1094 Ok(KclValue::none())
1095 }
1096 (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1097 let p1_x = KclValue::Number {
1098 value: p1_x.n,
1099 ty: p1_x.ty,
1100 meta: vec![args.source_range.into()],
1101 };
1102 let p1_y = KclValue::Number {
1103 value: p1_y.n,
1104 ty: p1_y.ty,
1105 meta: vec![args.source_range.into()],
1106 };
1107 let (constraint_x, constraint_y) =
1108 coincident_constraints_fixed(*p0_x, *p0_y, &p1_x, &p1_y, exec_state, &args)?;
1109
1110 #[cfg(feature = "artifact-graph")]
1111 let constraint_id = exec_state.next_object_id();
1112 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1114 return Err(KclError::new_semantic(KclErrorDetails::new(
1115 "coincident() can only be used inside a sketch block".to_owned(),
1116 vec![args.source_range],
1117 )));
1118 };
1119 sketch_state.solver_constraints.push(constraint_x);
1120 sketch_state.solver_constraints.push(constraint_y);
1121 #[cfg(feature = "artifact-graph")]
1122 {
1123 let constraint = crate::front::Constraint::Coincident(Coincident {
1124 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1125 });
1126 sketch_state.sketch_constraints.push(constraint_id);
1127 track_constraint(constraint_id, constraint, exec_state, &args);
1128 }
1129 Ok(KclValue::none())
1130 }
1131 (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1132 | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1133 Err(KclError::new_semantic(KclErrorDetails::new(
1135 "Unimplemented: When given points, input point at index 0 must be a sketch var for both x and y coordinates to constrain as coincident".to_owned(),
1136 vec![args.source_range],
1137 )))
1138 }
1139 }
1140 }
1141 (UnsolvedExpr::Known(p0_x), UnsolvedExpr::Known(p0_y)) => {
1142 let p1_x = &pos1[0];
1143 let p1_y = &pos1[1];
1144 match (p1_x, p1_y) {
1145 (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1146 let p0_x = KclValue::Number {
1147 value: p0_x.n,
1148 ty: p0_x.ty,
1149 meta: vec![args.source_range.into()],
1150 };
1151 let p0_y = KclValue::Number {
1152 value: p0_y.n,
1153 ty: p0_y.ty,
1154 meta: vec![args.source_range.into()],
1155 };
1156 let (constraint_x, constraint_y) =
1157 coincident_constraints_fixed(*p1_x, *p1_y, &p0_x, &p0_y, exec_state, &args)?;
1158
1159 #[cfg(feature = "artifact-graph")]
1160 let constraint_id = exec_state.next_object_id();
1161 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1163 return Err(KclError::new_semantic(KclErrorDetails::new(
1164 "coincident() can only be used inside a sketch block".to_owned(),
1165 vec![args.source_range],
1166 )));
1167 };
1168 sketch_state.solver_constraints.push(constraint_x);
1169 sketch_state.solver_constraints.push(constraint_y);
1170 #[cfg(feature = "artifact-graph")]
1171 {
1172 let constraint = crate::front::Constraint::Coincident(Coincident {
1173 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1174 });
1175 sketch_state.sketch_constraints.push(constraint_id);
1176 track_constraint(constraint_id, constraint, exec_state, &args);
1177 }
1178 Ok(KclValue::none())
1179 }
1180 (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1181 if *p0_x != *p1_x || *p0_y != *p1_y {
1182 return Err(KclError::new_semantic(KclErrorDetails::new(
1183 "Coincident constraint between two fixed points failed since coordinates differ"
1184 .to_owned(),
1185 vec![args.source_range],
1186 )));
1187 }
1188 Ok(KclValue::none())
1189 }
1190 (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1191 | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1192 Err(KclError::new_semantic(KclErrorDetails::new(
1194 "Unimplemented: When given points, input point at index 0 must be a sketch var for both x and y coordinates to constrain as coincident".to_owned(),
1195 vec![args.source_range],
1196 )))
1197 }
1198 }
1199 }
1200 (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1201 | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1202 Err(KclError::new_semantic(KclErrorDetails::new(
1204 "When given points, input point at index 0 must be a sketch var for both x and y coordinates to constrain as coincident".to_owned(),
1205 vec![args.source_range],
1206 )))
1207 }
1208 }
1209 }
1210 (
1212 UnsolvedSegmentKind::Point {
1213 position: point_pos, ..
1214 },
1215 UnsolvedSegmentKind::Line {
1216 start: line_start,
1217 end: line_end,
1218 ..
1219 },
1220 )
1221 | (
1222 UnsolvedSegmentKind::Line {
1223 start: line_start,
1224 end: line_end,
1225 ..
1226 },
1227 UnsolvedSegmentKind::Point {
1228 position: point_pos, ..
1229 },
1230 ) => {
1231 let point_x = &point_pos[0];
1232 let point_y = &point_pos[1];
1233 match (point_x, point_y) {
1234 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1235 let (start_x, start_y) = (&line_start[0], &line_start[1]);
1237 let (end_x, end_y) = (&line_end[0], &line_end[1]);
1238
1239 match (start_x, start_y, end_x, end_y) {
1240 (
1241 UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1242 UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1243 ) => {
1244 let point = DatumPoint::new_xy(
1245 point_x.to_constraint_id(range)?,
1246 point_y.to_constraint_id(range)?,
1247 );
1248 let line_segment = DatumLineSegment::new(
1249 DatumPoint::new_xy(sx.to_constraint_id(range)?, sy.to_constraint_id(range)?),
1250 DatumPoint::new_xy(ex.to_constraint_id(range)?, ey.to_constraint_id(range)?),
1251 );
1252 let constraint = SolverConstraint::PointLineDistance(point, line_segment, 0.0);
1253
1254 #[cfg(feature = "artifact-graph")]
1255 let constraint_id = exec_state.next_object_id();
1256
1257 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1258 return Err(KclError::new_semantic(KclErrorDetails::new(
1259 "coincident() can only be used inside a sketch block".to_owned(),
1260 vec![args.source_range],
1261 )));
1262 };
1263 sketch_state.solver_constraints.push(constraint);
1264 #[cfg(feature = "artifact-graph")]
1265 {
1266 let constraint = crate::front::Constraint::Coincident(Coincident {
1267 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1268 });
1269 sketch_state.sketch_constraints.push(constraint_id);
1270 track_constraint(constraint_id, constraint, exec_state, &args);
1271 }
1272 Ok(KclValue::none())
1273 }
1274 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1275 "Line segment endpoints must be sketch variables for point-segment coincident constraint".to_owned(),
1276 vec![args.source_range],
1277 ))),
1278 }
1279 }
1280 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1281 "Point coordinates must be sketch variables for point-segment coincident constraint"
1282 .to_owned(),
1283 vec![args.source_range],
1284 ))),
1285 }
1286 }
1287 (
1289 UnsolvedSegmentKind::Point {
1290 position: point_pos, ..
1291 },
1292 UnsolvedSegmentKind::Arc {
1293 start: arc_start,
1294 end: arc_end,
1295 center: arc_center,
1296 ..
1297 },
1298 )
1299 | (
1300 UnsolvedSegmentKind::Arc {
1301 start: arc_start,
1302 end: arc_end,
1303 center: arc_center,
1304 ..
1305 },
1306 UnsolvedSegmentKind::Point {
1307 position: point_pos, ..
1308 },
1309 ) => {
1310 let point_x = &point_pos[0];
1311 let point_y = &point_pos[1];
1312 match (point_x, point_y) {
1313 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1314 let (center_x, center_y) = (&arc_center[0], &arc_center[1]);
1316 let (start_x, start_y) = (&arc_start[0], &arc_start[1]);
1317 let (end_x, end_y) = (&arc_end[0], &arc_end[1]);
1318
1319 match (center_x, center_y, start_x, start_y, end_x, end_y) {
1320 (
1321 UnsolvedExpr::Unknown(cx), UnsolvedExpr::Unknown(cy),
1322 UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1323 UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1324 ) => {
1325 let point = DatumPoint::new_xy(
1326 point_x.to_constraint_id(range)?,
1327 point_y.to_constraint_id(range)?,
1328 );
1329 let circular_arc = DatumCircularArc {
1330 center: DatumPoint::new_xy(
1331 cx.to_constraint_id(range)?,
1332 cy.to_constraint_id(range)?,
1333 ),
1334 start: DatumPoint::new_xy(
1335 sx.to_constraint_id(range)?,
1336 sy.to_constraint_id(range)?,
1337 ),
1338 end: DatumPoint::new_xy(
1339 ex.to_constraint_id(range)?,
1340 ey.to_constraint_id(range)?,
1341 ),
1342 };
1343 let constraint = SolverConstraint::PointArcCoincident(circular_arc, point);
1344
1345 #[cfg(feature = "artifact-graph")]
1346 let constraint_id = exec_state.next_object_id();
1347
1348 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1349 return Err(KclError::new_semantic(KclErrorDetails::new(
1350 "coincident() can only be used inside a sketch block".to_owned(),
1351 vec![args.source_range],
1352 )));
1353 };
1354 sketch_state.solver_constraints.push(constraint);
1355 #[cfg(feature = "artifact-graph")]
1356 {
1357 let constraint = crate::front::Constraint::Coincident(Coincident {
1358 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1359 });
1360 sketch_state.sketch_constraints.push(constraint_id);
1361 track_constraint(constraint_id, constraint, exec_state, &args);
1362 }
1363 Ok(KclValue::none())
1364 }
1365 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1366 "Arc center, start, and end points must be sketch variables for point-arc coincident constraint".to_owned(),
1367 vec![args.source_range],
1368 ))),
1369 }
1370 }
1371 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1372 "Point coordinates must be sketch variables for point-arc coincident constraint".to_owned(),
1373 vec![args.source_range],
1374 ))),
1375 }
1376 }
1377 (
1380 UnsolvedSegmentKind::Point {
1381 position: point_pos, ..
1382 },
1383 UnsolvedSegmentKind::Circle {
1384 start: circle_start,
1385 center: circle_center,
1386 ..
1387 },
1388 )
1389 | (
1390 UnsolvedSegmentKind::Circle {
1391 start: circle_start,
1392 center: circle_center,
1393 ..
1394 },
1395 UnsolvedSegmentKind::Point {
1396 position: point_pos, ..
1397 },
1398 ) => {
1399 let point_x = &point_pos[0];
1400 let point_y = &point_pos[1];
1401 match (point_x, point_y) {
1402 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1403 let (center_x, center_y) = (&circle_center[0], &circle_center[1]);
1405 let (start_x, start_y) = (&circle_start[0], &circle_start[1]);
1406
1407 match (center_x, center_y, start_x, start_y) {
1408 (
1409 UnsolvedExpr::Unknown(cx),
1410 UnsolvedExpr::Unknown(cy),
1411 UnsolvedExpr::Unknown(sx),
1412 UnsolvedExpr::Unknown(sy),
1413 ) => {
1414 let point_radius_line = DatumLineSegment::new(
1415 DatumPoint::new_xy(
1416 cx.to_constraint_id(range)?,
1417 cy.to_constraint_id(range)?,
1418 ),
1419 DatumPoint::new_xy(
1420 point_x.to_constraint_id(range)?,
1421 point_y.to_constraint_id(range)?,
1422 ),
1423 );
1424 let circle_radius_line = DatumLineSegment::new(
1425 DatumPoint::new_xy(
1426 cx.to_constraint_id(range)?,
1427 cy.to_constraint_id(range)?,
1428 ),
1429 DatumPoint::new_xy(
1430 sx.to_constraint_id(range)?,
1431 sy.to_constraint_id(range)?,
1432 ),
1433 );
1434 let constraint =
1435 SolverConstraint::LinesEqualLength(point_radius_line, circle_radius_line);
1436
1437 #[cfg(feature = "artifact-graph")]
1438 let constraint_id = exec_state.next_object_id();
1439
1440 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1441 return Err(KclError::new_semantic(KclErrorDetails::new(
1442 "coincident() can only be used inside a sketch block".to_owned(),
1443 vec![args.source_range],
1444 )));
1445 };
1446 sketch_state.solver_constraints.push(constraint);
1447 #[cfg(feature = "artifact-graph")]
1448 {
1449 let constraint = crate::front::Constraint::Coincident(Coincident {
1450 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1451 });
1452 sketch_state.sketch_constraints.push(constraint_id);
1453 track_constraint(constraint_id, constraint, exec_state, &args);
1454 }
1455 Ok(KclValue::none())
1456 }
1457 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1458 "Circle start and center points must be sketch variables for point-circle coincident constraint".to_owned(),
1459 vec![args.source_range],
1460 ))),
1461 }
1462 }
1463 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1464 "Point coordinates must be sketch variables for point-circle coincident constraint"
1465 .to_owned(),
1466 vec![args.source_range],
1467 ))),
1468 }
1469 }
1470 (
1472 UnsolvedSegmentKind::Line {
1473 start: line0_start,
1474 end: line0_end,
1475 ..
1476 },
1477 UnsolvedSegmentKind::Line {
1478 start: line1_start,
1479 end: line1_end,
1480 ..
1481 },
1482 ) => {
1483 let (line0_start_x, line0_start_y) = (&line0_start[0], &line0_start[1]);
1485 let (line0_end_x, line0_end_y) = (&line0_end[0], &line0_end[1]);
1486 let (line1_start_x, line1_start_y) = (&line1_start[0], &line1_start[1]);
1487 let (line1_end_x, line1_end_y) = (&line1_end[0], &line1_end[1]);
1488
1489 match (
1490 line0_start_x,
1491 line0_start_y,
1492 line0_end_x,
1493 line0_end_y,
1494 line1_start_x,
1495 line1_start_y,
1496 line1_end_x,
1497 line1_end_y,
1498 ) {
1499 (
1500 UnsolvedExpr::Unknown(l0_sx),
1501 UnsolvedExpr::Unknown(l0_sy),
1502 UnsolvedExpr::Unknown(l0_ex),
1503 UnsolvedExpr::Unknown(l0_ey),
1504 UnsolvedExpr::Unknown(l1_sx),
1505 UnsolvedExpr::Unknown(l1_sy),
1506 UnsolvedExpr::Unknown(l1_ex),
1507 UnsolvedExpr::Unknown(l1_ey),
1508 ) => {
1509 let line0_segment = DatumLineSegment::new(
1511 DatumPoint::new_xy(l0_sx.to_constraint_id(range)?, l0_sy.to_constraint_id(range)?),
1512 DatumPoint::new_xy(l0_ex.to_constraint_id(range)?, l0_ey.to_constraint_id(range)?),
1513 );
1514 let line1_segment = DatumLineSegment::new(
1515 DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?),
1516 DatumPoint::new_xy(l1_ex.to_constraint_id(range)?, l1_ey.to_constraint_id(range)?),
1517 );
1518
1519 let parallel_constraint =
1521 SolverConstraint::LinesAtAngle(line0_segment, line1_segment, AngleKind::Parallel);
1522
1523 let point_on_line1 =
1525 DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?);
1526 let distance_constraint =
1527 SolverConstraint::PointLineDistance(point_on_line1, line0_segment, 0.0);
1528
1529 #[cfg(feature = "artifact-graph")]
1530 let constraint_id = exec_state.next_object_id();
1531
1532 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1533 return Err(KclError::new_semantic(KclErrorDetails::new(
1534 "coincident() can only be used inside a sketch block".to_owned(),
1535 vec![args.source_range],
1536 )));
1537 };
1538 sketch_state.solver_constraints.push(parallel_constraint);
1540 sketch_state.solver_constraints.push(distance_constraint);
1541 #[cfg(feature = "artifact-graph")]
1542 {
1543 let constraint = crate::front::Constraint::Coincident(Coincident {
1544 segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1545 });
1546 sketch_state.sketch_constraints.push(constraint_id);
1547 track_constraint(constraint_id, constraint, exec_state, &args);
1548 }
1549 Ok(KclValue::none())
1550 }
1551 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1552 "Line segment endpoints must be sketch variables for line-line coincident constraint"
1553 .to_owned(),
1554 vec![args.source_range],
1555 ))),
1556 }
1557 }
1558 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1559 format!(
1560 "coincident supports point-point, point-segment, or segment-segment; found {:?} and {:?}",
1561 &unsolved0.kind, &unsolved1.kind
1562 ),
1563 vec![args.source_range],
1564 ))),
1565 }
1566 }
1567 (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1570 let Some(pt) = <[TyF64; 2]>::from_kcl_val(point2d) else {
1571 return Err(KclError::new_semantic(KclErrorDetails::new(
1572 "Expected a Segment or Point2d (e.g. [1mm, 2mm])".to_owned(),
1573 vec![args.source_range],
1574 )));
1575 };
1576 let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1577 return Err(KclError::new_semantic(KclErrorDetails::new(
1578 "segment must be an unsolved segment".to_owned(),
1579 vec![args.source_range],
1580 )));
1581 };
1582 match &unsolved.kind {
1583 UnsolvedSegmentKind::Point { position, .. } => {
1584 let p_x = &position[0];
1585 let p_y = &position[1];
1586 match (p_x, p_y) {
1587 (UnsolvedExpr::Unknown(p_x), UnsolvedExpr::Unknown(p_y)) => {
1588 let pt_x = KclValue::Number {
1589 value: pt[0].n,
1590 ty: pt[0].ty,
1591 meta: vec![args.source_range.into()],
1592 };
1593 let pt_y = KclValue::Number {
1594 value: pt[1].n,
1595 ty: pt[1].ty,
1596 meta: vec![args.source_range.into()],
1597 };
1598 let (constraint_x, constraint_y) =
1599 coincident_constraints_fixed(*p_x, *p_y, &pt_x, &pt_y, exec_state, &args)?;
1600
1601 #[cfg(feature = "artifact-graph")]
1602 let constraint_id = exec_state.next_object_id();
1603 #[cfg(feature = "artifact-graph")]
1604 let coincident_segments = coincident_segments_for_segment_and_point2d(
1605 unsolved.object_id,
1606 point2d,
1607 matches!((&point0, &point1), (KclValue::Segment { .. }, _)),
1608 );
1609 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1610 return Err(KclError::new_semantic(KclErrorDetails::new(
1611 "coincident() can only be used inside a sketch block".to_owned(),
1612 vec![args.source_range],
1613 )));
1614 };
1615 sketch_state.solver_constraints.push(constraint_x);
1616 sketch_state.solver_constraints.push(constraint_y);
1617 #[cfg(feature = "artifact-graph")]
1618 {
1619 let constraint = crate::front::Constraint::Coincident(Coincident {
1620 segments: coincident_segments,
1621 });
1622 sketch_state.sketch_constraints.push(constraint_id);
1623 track_constraint(constraint_id, constraint, exec_state, &args);
1624 }
1625 Ok(KclValue::none())
1626 }
1627 (UnsolvedExpr::Known(known_x), UnsolvedExpr::Known(known_y)) => {
1628 let pt_x_val = normalize_to_solver_distance_unit(
1629 &KclValue::Number {
1630 value: pt[0].n,
1631 ty: pt[0].ty,
1632 meta: vec![args.source_range.into()],
1633 },
1634 args.source_range,
1635 exec_state,
1636 "coincident constraint value",
1637 )?;
1638 let pt_y_val = normalize_to_solver_distance_unit(
1639 &KclValue::Number {
1640 value: pt[1].n,
1641 ty: pt[1].ty,
1642 meta: vec![args.source_range.into()],
1643 },
1644 args.source_range,
1645 exec_state,
1646 "coincident constraint value",
1647 )?;
1648 let Some(pt_x) = pt_x_val.as_ty_f64() else {
1649 return Err(KclError::new_semantic(KclErrorDetails::new(
1650 "Expected number for Point2d x coordinate".to_owned(),
1651 vec![args.source_range],
1652 )));
1653 };
1654 let Some(pt_y) = pt_y_val.as_ty_f64() else {
1655 return Err(KclError::new_semantic(KclErrorDetails::new(
1656 "Expected number for Point2d y coordinate".to_owned(),
1657 vec![args.source_range],
1658 )));
1659 };
1660 let known_x_val = normalize_to_solver_distance_unit(
1661 &KclValue::Number {
1662 value: known_x.n,
1663 ty: known_x.ty,
1664 meta: vec![args.source_range.into()],
1665 },
1666 args.source_range,
1667 exec_state,
1668 "coincident constraint value",
1669 )?;
1670 let Some(known_x_f) = known_x_val.as_ty_f64() else {
1671 return Err(KclError::new_semantic(KclErrorDetails::new(
1672 "Expected number for known x coordinate".to_owned(),
1673 vec![args.source_range],
1674 )));
1675 };
1676 let known_y_val = normalize_to_solver_distance_unit(
1677 &KclValue::Number {
1678 value: known_y.n,
1679 ty: known_y.ty,
1680 meta: vec![args.source_range.into()],
1681 },
1682 args.source_range,
1683 exec_state,
1684 "coincident constraint value",
1685 )?;
1686 let Some(known_y_f) = known_y_val.as_ty_f64() else {
1687 return Err(KclError::new_semantic(KclErrorDetails::new(
1688 "Expected number for known y coordinate".to_owned(),
1689 vec![args.source_range],
1690 )));
1691 };
1692 if known_x_f.n != pt_x.n || known_y_f.n != pt_y.n {
1693 return Err(KclError::new_semantic(KclErrorDetails::new(
1694 "Coincident constraint between two fixed points failed since coordinates differ"
1695 .to_owned(),
1696 vec![args.source_range],
1697 )));
1698 }
1699 Ok(KclValue::none())
1700 }
1701 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1702 "Point coordinates must have consistent known/unknown status for coincident constraint"
1703 .to_owned(),
1704 vec![args.source_range],
1705 ))),
1706 }
1707 }
1708 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1709 "A Point2d can only be constrained coincident with a point segment, not a line or arc".to_owned(),
1710 vec![args.source_range],
1711 ))),
1712 }
1713 }
1714 _ => {
1716 let pt0 = <[TyF64; 2]>::from_kcl_val(&point0);
1717 let pt1 = <[TyF64; 2]>::from_kcl_val(&point1);
1718 match (pt0, pt1) {
1719 (Some(a), Some(b)) => {
1720 let a_x = normalize_to_solver_distance_unit(
1722 &KclValue::Number {
1723 value: a[0].n,
1724 ty: a[0].ty,
1725 meta: vec![args.source_range.into()],
1726 },
1727 args.source_range,
1728 exec_state,
1729 "coincident constraint value",
1730 )?;
1731 let a_y = normalize_to_solver_distance_unit(
1732 &KclValue::Number {
1733 value: a[1].n,
1734 ty: a[1].ty,
1735 meta: vec![args.source_range.into()],
1736 },
1737 args.source_range,
1738 exec_state,
1739 "coincident constraint value",
1740 )?;
1741 let b_x = normalize_to_solver_distance_unit(
1742 &KclValue::Number {
1743 value: b[0].n,
1744 ty: b[0].ty,
1745 meta: vec![args.source_range.into()],
1746 },
1747 args.source_range,
1748 exec_state,
1749 "coincident constraint value",
1750 )?;
1751 let b_y = normalize_to_solver_distance_unit(
1752 &KclValue::Number {
1753 value: b[1].n,
1754 ty: b[1].ty,
1755 meta: vec![args.source_range.into()],
1756 },
1757 args.source_range,
1758 exec_state,
1759 "coincident constraint value",
1760 )?;
1761 if a_x.as_ty_f64().map(|v| v.n) != b_x.as_ty_f64().map(|v| v.n)
1762 || a_y.as_ty_f64().map(|v| v.n) != b_y.as_ty_f64().map(|v| v.n)
1763 {
1764 return Err(KclError::new_semantic(KclErrorDetails::new(
1765 "Coincident constraint between two fixed points failed since coordinates differ".to_owned(),
1766 vec![args.source_range],
1767 )));
1768 }
1769 Ok(KclValue::none())
1770 }
1771 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1772 "All inputs must be Segments or Point2d values".to_owned(),
1773 vec![args.source_range],
1774 ))),
1775 }
1776 }
1777 }
1778}
1779
1780fn coincident_points(
1781 point_values: Vec<KclValue>,
1782 exec_state: &mut ExecState,
1783 args: Args,
1784) -> Result<KclValue, KclError> {
1785 if point_values.len() < 2 {
1786 return Err(KclError::new_semantic(KclErrorDetails::new(
1787 "coincident() point list must contain at least two points".to_owned(),
1788 vec![args.source_range],
1789 )));
1790 }
1791
1792 let points = point_values
1794 .iter()
1795 .map(|point| extract_multi_coincident_point(point, args.source_range))
1796 .collect::<Result<Vec<_>, _>>()?;
1797
1798 #[cfg(feature = "artifact-graph")]
1799 let constraint_segments = points.iter().map(|point| point.constraint_segment).collect::<Vec<_>>();
1800
1801 let mut variable_points = Vec::new();
1802 let mut fixed_points = Vec::new();
1803 for point in points {
1804 match point.point {
1805 PointToAlign::Variable { x, y } => variable_points.push([x, y]),
1806 PointToAlign::Fixed { x, y } => fixed_points.push([x, y]),
1807 }
1808 }
1809
1810 let mut solver_constraints = Vec::with_capacity(point_values.len().saturating_sub(1) * 2);
1811 if let Some((anchor_fixed, remaining_fixed_points)) = fixed_points.split_first() {
1812 if remaining_fixed_points
1814 .iter()
1815 .any(|point| !fixed_points_match(point, anchor_fixed))
1816 {
1817 return Err(KclError::new_semantic(KclErrorDetails::new(
1818 "coincident() with more than two inputs can include at most one fixed point location".to_owned(),
1819 vec![args.source_range],
1820 )));
1821 }
1822
1823 let anchor_x = ty_f64_to_kcl_value(anchor_fixed[0].clone(), args.source_range);
1824 let anchor_y = ty_f64_to_kcl_value(anchor_fixed[1].clone(), args.source_range);
1825 for point in variable_points {
1826 let (constraint_x, constraint_y) =
1827 coincident_constraints_fixed(point[0], point[1], &anchor_x, &anchor_y, exec_state, &args)?;
1828 solver_constraints.push(constraint_x);
1829 solver_constraints.push(constraint_y);
1830 }
1831 } else {
1832 let mut points = variable_points.into_iter();
1834 let first_point = points.next().ok_or_else(|| {
1835 KclError::new_semantic(KclErrorDetails::new(
1836 "coincident() point list must contain at least two points".to_owned(),
1837 vec![args.source_range],
1838 ))
1839 })?;
1840 let anchor = datum_point(first_point, args.source_range)?;
1841 for point in points {
1842 let solver_point = datum_point(point, args.source_range)?;
1843 solver_constraints.push(SolverConstraint::PointsCoincident(anchor, solver_point));
1844 }
1845 }
1846
1847 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1848 return Err(KclError::new_semantic(KclErrorDetails::new(
1849 "coincident() can only be used inside a sketch block".to_owned(),
1850 vec![args.source_range],
1851 )));
1852 };
1853 sketch_state.solver_constraints.extend(solver_constraints);
1854
1855 #[cfg(feature = "artifact-graph")]
1856 {
1857 let constraint_id = exec_state.next_object_id();
1859 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1860 debug_assert!(false, "Constraint created outside a sketch block");
1861 return Ok(KclValue::none());
1862 };
1863 sketch_state.sketch_constraints.push(constraint_id);
1864 let constraint = Constraint::Coincident(Coincident {
1865 segments: constraint_segments,
1866 });
1867 track_constraint(constraint_id, constraint, exec_state, &args);
1868 }
1869
1870 Ok(KclValue::none())
1871}
1872
1873fn extract_multi_coincident_point(
1874 input: &KclValue,
1875 source_range: crate::SourceRange,
1876) -> Result<CoincidentPointInput, KclError> {
1877 match input {
1879 KclValue::Segment { value: segment } => {
1880 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
1881 return Err(KclError::new_semantic(KclErrorDetails::new(
1882 "coincident() with more than two inputs only supports unsolved points or ORIGIN".to_owned(),
1883 vec![source_range],
1884 )));
1885 };
1886 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
1887 return Err(KclError::new_semantic(KclErrorDetails::new(
1888 format!(
1889 "coincident() with more than two inputs only supports points or ORIGIN, but one item is {}",
1890 unsolved.kind.human_friendly_kind_with_article()
1891 ),
1892 vec![source_range],
1893 )));
1894 };
1895 match (&position[0], &position[1]) {
1896 (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(CoincidentPointInput {
1897 point: PointToAlign::Fixed {
1898 x: x.to_owned(),
1899 y: y.to_owned(),
1900 },
1901 #[cfg(feature = "artifact-graph")]
1902 constraint_segment: unsolved.object_id.into(),
1903 }),
1904 (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(CoincidentPointInput {
1905 point: PointToAlign::Variable { x: *x, y: *y },
1906 #[cfg(feature = "artifact-graph")]
1907 constraint_segment: unsolved.object_id.into(),
1908 }),
1909 (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..))
1911 | (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => Err(KclError::new_semantic(
1912 KclErrorDetails::new(
1913 "coincident() with more than two inputs requires each point to be fully fixed or fully variable"
1914 .to_owned(),
1915 vec![source_range],
1916 ),
1917 )),
1918 }
1919 }
1920 point if point2d_is_origin(point) => {
1921 let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point) else {
1922 debug_assert!(false, "Origin literal should coerce to Point2d");
1923 return Err(KclError::new_internal(KclErrorDetails::new(
1924 "Origin literal could not be converted to a point".to_owned(),
1925 vec![source_range],
1926 )));
1927 };
1928 Ok(CoincidentPointInput {
1929 point: PointToAlign::Fixed { x, y },
1930 #[cfg(feature = "artifact-graph")]
1931 constraint_segment: ConstraintSegment::ORIGIN,
1932 })
1933 }
1934 _ => Err(KclError::new_semantic(KclErrorDetails::new(
1935 "coincident() with more than two inputs only supports points and ORIGIN".to_owned(),
1936 vec![source_range],
1937 ))),
1938 }
1939}
1940
1941#[derive(Debug, Clone)]
1942struct CoincidentPointInput {
1943 point: PointToAlign,
1944 #[cfg(feature = "artifact-graph")]
1945 constraint_segment: ConstraintSegment,
1946}
1947
1948fn fixed_points_match(a: &[TyF64; 2], b: &[TyF64; 2]) -> bool {
1949 a[0].to_mm() == b[0].to_mm() && a[1].to_mm() == b[1].to_mm()
1950}
1951
1952fn ty_f64_to_kcl_value(value: TyF64, source_range: crate::SourceRange) -> KclValue {
1953 KclValue::Number {
1954 value: value.n,
1955 ty: value.ty,
1956 meta: vec![source_range.into()],
1957 }
1958}
1959
1960#[cfg(feature = "artifact-graph")]
1961fn track_constraint(constraint_id: ObjectId, constraint: Constraint, exec_state: &mut ExecState, args: &Args) {
1962 let sketch_id = {
1963 let Some(sketch_state) = exec_state.sketch_block_mut() else {
1964 debug_assert!(false, "Constraint created outside a sketch block");
1965 return;
1966 };
1967 sketch_state.sketch_id
1968 };
1969 let Some(sketch_id) = sketch_id else {
1970 debug_assert!(false, "Constraint created without a sketch id");
1971 return;
1972 };
1973 let artifact_id = exec_state.next_artifact_id();
1974 exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
1975 id: artifact_id,
1976 sketch_id,
1977 constraint_id,
1978 constraint_type: SketchBlockConstraintType::from(&constraint),
1979 code_ref: CodeRef::placeholder(args.source_range),
1980 }));
1981 exec_state.add_scene_object(
1982 Object {
1983 id: constraint_id,
1984 kind: ObjectKind::Constraint { constraint },
1985 label: Default::default(),
1986 comments: Default::default(),
1987 artifact_id,
1988 source: SourceRef::new(args.source_range, args.node_path.clone()),
1989 },
1990 args.source_range,
1991 );
1992}
1993
1994fn coincident_constraints_fixed(
1996 p0_x: SketchVarId,
1997 p0_y: SketchVarId,
1998 p1_x: &KclValue,
1999 p1_y: &KclValue,
2000 exec_state: &mut ExecState,
2001 args: &Args,
2002) -> Result<(ezpz::Constraint, ezpz::Constraint), KclError> {
2003 let p1_x_number_value =
2004 normalize_to_solver_distance_unit(p1_x, p1_x.into(), exec_state, "coincident constraint value")?;
2005 let p1_y_number_value =
2006 normalize_to_solver_distance_unit(p1_y, p1_y.into(), exec_state, "coincident constraint value")?;
2007 let Some(p1_x) = p1_x_number_value.as_ty_f64() else {
2008 let message = format!(
2009 "Expected number after coercion, but found {}",
2010 p1_x_number_value.human_friendly_type()
2011 );
2012 debug_assert!(false, "{}", &message);
2013 return Err(KclError::new_internal(KclErrorDetails::new(
2014 message,
2015 vec![args.source_range],
2016 )));
2017 };
2018 let Some(p1_y) = p1_y_number_value.as_ty_f64() else {
2019 let message = format!(
2020 "Expected number after coercion, but found {}",
2021 p1_y_number_value.human_friendly_type()
2022 );
2023 debug_assert!(false, "{}", &message);
2024 return Err(KclError::new_internal(KclErrorDetails::new(
2025 message,
2026 vec![args.source_range],
2027 )));
2028 };
2029 let constraint_x = SolverConstraint::Fixed(p0_x.to_constraint_id(args.source_range)?, p1_x.n);
2030 let constraint_y = SolverConstraint::Fixed(p0_y.to_constraint_id(args.source_range)?, p1_y.n);
2031 Ok((constraint_x, constraint_y))
2032}
2033
2034pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2035 let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2036 "points",
2037 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2038 exec_state,
2039 )?;
2040 let label_position = get_constraint_label_position(exec_state, &args, "distance")?;
2041 let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
2042 KclError::new_semantic(KclErrorDetails::new(
2043 "must have two input points".to_owned(),
2044 vec![args.source_range],
2045 ))
2046 })?;
2047
2048 match (&point0, &point1) {
2049 (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2050 let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2051 return Err(KclError::new_semantic(KclErrorDetails::new(
2052 "first point must be an unsolved segment".to_owned(),
2053 vec![args.source_range],
2054 )));
2055 };
2056 let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2057 return Err(KclError::new_semantic(KclErrorDetails::new(
2058 "second point must be an unsolved segment".to_owned(),
2059 vec![args.source_range],
2060 )));
2061 };
2062 match (&unsolved0.kind, &unsolved1.kind) {
2063 (
2064 UnsolvedSegmentKind::Point { position: pos0, .. },
2065 UnsolvedSegmentKind::Point { position: pos1, .. },
2066 ) => {
2067 match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2070 (
2071 UnsolvedExpr::Unknown(p0_x),
2072 UnsolvedExpr::Unknown(p0_y),
2073 UnsolvedExpr::Unknown(p1_x),
2074 UnsolvedExpr::Unknown(p1_y),
2075 ) => {
2076 let sketch_constraint = SketchConstraint {
2078 kind: SketchConstraintKind::Distance {
2079 points: [
2080 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2081 vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2082 object_id: unsolved0.object_id,
2083 }),
2084 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2085 vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2086 object_id: unsolved1.object_id,
2087 }),
2088 ],
2089 label_position,
2090 },
2091 meta: vec![args.source_range.into()],
2092 };
2093 Ok(KclValue::SketchConstraint {
2094 value: Box::new(sketch_constraint),
2095 })
2096 }
2097 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2098 "unimplemented: distance() arguments must be all sketch vars in all coordinates".to_owned(),
2099 vec![args.source_range],
2100 ))),
2101 }
2102 }
2103 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2104 "distance() arguments must be unsolved points".to_owned(),
2105 vec![args.source_range],
2106 ))),
2107 }
2108 }
2109 (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2111 if !point2d_is_origin(point2d) {
2112 return Err(KclError::new_semantic(KclErrorDetails::new(
2113 "distance() Point2d arguments must be ORIGIN".to_owned(),
2114 vec![args.source_range],
2115 )));
2116 }
2117
2118 let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2119 return Err(KclError::new_semantic(KclErrorDetails::new(
2120 "segment must be an unsolved segment".to_owned(),
2121 vec![args.source_range],
2122 )));
2123 };
2124 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2125 return Err(KclError::new_semantic(KclErrorDetails::new(
2126 "distance() arguments must be unsolved points or ORIGIN".to_owned(),
2127 vec![args.source_range],
2128 )));
2129 };
2130 match (&position[0], &position[1]) {
2131 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2132 let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2133 vars: crate::front::Point2d {
2134 x: *point_x,
2135 y: *point_y,
2136 },
2137 object_id: unsolved.object_id,
2138 });
2139 let points = if matches!((&point0, &point1), (KclValue::Segment { .. }, _)) {
2140 [point, ConstrainablePoint2dOrOrigin::Origin]
2141 } else {
2142 [ConstrainablePoint2dOrOrigin::Origin, point]
2143 };
2144 Ok(KclValue::SketchConstraint {
2145 value: Box::new(SketchConstraint {
2146 kind: SketchConstraintKind::Distance { points, label_position },
2147 meta: vec![args.source_range.into()],
2148 }),
2149 })
2150 }
2151 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2152 "unimplemented: distance() point arguments must be sketch vars in all coordinates".to_owned(),
2153 vec![args.source_range],
2154 ))),
2155 }
2156 }
2157 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2158 "distance() arguments must be point segments or ORIGIN".to_owned(),
2159 vec![args.source_range],
2160 ))),
2161 }
2162}
2163
2164fn get_constraint_label_position(
2165 exec_state: &mut ExecState,
2166 args: &Args,
2167 constraint_name: &str,
2168) -> Result<Option<Point2d<Number>>, KclError> {
2169 let label_position = args.get_kw_arg_opt::<[TyF64; 2]>("labelPosition", &RuntimeType::point2d(), exec_state)?;
2170
2171 label_position
2172 .map(|label| {
2173 TyF64::to_point2d(&label).map_err(|_| {
2174 KclError::new_internal(KclErrorDetails::new(
2175 format!("Could not convert {constraint_name} label position to a Point2d"),
2176 vec![args.source_range],
2177 ))
2178 })
2179 })
2180 .transpose()
2181}
2182
2183fn create_circular_radius_constraint(
2186 segment: KclValue,
2187 constraint_kind: fn([ConstrainablePoint2d; 2]) -> SketchConstraintKind,
2188 source_range: crate::SourceRange,
2189) -> Result<SketchConstraint, KclError> {
2190 let dummy_constraint = constraint_kind([
2192 ConstrainablePoint2d {
2193 vars: crate::front::Point2d {
2194 x: SketchVarId(0),
2195 y: SketchVarId(0),
2196 },
2197 object_id: ObjectId(0),
2198 },
2199 ConstrainablePoint2d {
2200 vars: crate::front::Point2d {
2201 x: SketchVarId(0),
2202 y: SketchVarId(0),
2203 },
2204 object_id: ObjectId(0),
2205 },
2206 ]);
2207 let function_name = dummy_constraint.name();
2208
2209 let KclValue::Segment { value: seg } = segment else {
2210 return Err(KclError::new_semantic(KclErrorDetails::new(
2211 format!("{}() argument must be a segment", function_name),
2212 vec![source_range],
2213 )));
2214 };
2215 let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2216 return Err(KclError::new_semantic(KclErrorDetails::new(
2217 "segment must be unsolved".to_owned(),
2218 vec![source_range],
2219 )));
2220 };
2221 match &unsolved.kind {
2222 UnsolvedSegmentKind::Arc {
2223 center,
2224 start,
2225 center_object_id,
2226 start_object_id,
2227 ..
2228 }
2229 | UnsolvedSegmentKind::Circle {
2230 center,
2231 start,
2232 center_object_id,
2233 start_object_id,
2234 ..
2235 } => {
2236 match (¢er[0], ¢er[1], &start[0], &start[1]) {
2238 (
2239 UnsolvedExpr::Unknown(center_x),
2240 UnsolvedExpr::Unknown(center_y),
2241 UnsolvedExpr::Unknown(start_x),
2242 UnsolvedExpr::Unknown(start_y),
2243 ) => {
2244 let sketch_constraint = SketchConstraint {
2246 kind: constraint_kind([
2247 ConstrainablePoint2d {
2248 vars: crate::front::Point2d {
2249 x: *center_x,
2250 y: *center_y,
2251 },
2252 object_id: *center_object_id,
2253 },
2254 ConstrainablePoint2d {
2255 vars: crate::front::Point2d {
2256 x: *start_x,
2257 y: *start_y,
2258 },
2259 object_id: *start_object_id,
2260 },
2261 ]),
2262 meta: vec![source_range.into()],
2263 };
2264 Ok(sketch_constraint)
2265 }
2266 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2267 format!(
2268 "unimplemented: {}() arc or circle segment must have all sketch vars in all coordinates",
2269 function_name
2270 ),
2271 vec![source_range],
2272 ))),
2273 }
2274 }
2275 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2276 format!("{}() argument must be an arc or circle segment", function_name),
2277 vec![source_range],
2278 ))),
2279 }
2280}
2281
2282pub async fn radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2283 let segment: KclValue =
2284 args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2285
2286 create_circular_radius_constraint(
2287 segment,
2288 |points| SketchConstraintKind::Radius { points },
2289 args.source_range,
2290 )
2291 .map(|constraint| KclValue::SketchConstraint {
2292 value: Box::new(constraint),
2293 })
2294}
2295
2296pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2297 let segment: KclValue =
2298 args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2299
2300 create_circular_radius_constraint(
2301 segment,
2302 |points| SketchConstraintKind::Diameter { points },
2303 args.source_range,
2304 )
2305 .map(|constraint| KclValue::SketchConstraint {
2306 value: Box::new(constraint),
2307 })
2308}
2309
2310pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2311 let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2312 "points",
2313 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2314 exec_state,
2315 )?;
2316 let label_position = get_constraint_label_position(exec_state, &args, "horizontalDistance")?;
2317 let [p1, p2] = points.as_slice() else {
2318 return Err(KclError::new_semantic(KclErrorDetails::new(
2319 "must have two input points".to_owned(),
2320 vec![args.source_range],
2321 )));
2322 };
2323 match (p1, p2) {
2324 (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2325 let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2326 return Err(KclError::new_semantic(KclErrorDetails::new(
2327 "first point must be an unsolved segment".to_owned(),
2328 vec![args.source_range],
2329 )));
2330 };
2331 let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2332 return Err(KclError::new_semantic(KclErrorDetails::new(
2333 "second point must be an unsolved segment".to_owned(),
2334 vec![args.source_range],
2335 )));
2336 };
2337 match (&unsolved0.kind, &unsolved1.kind) {
2338 (
2339 UnsolvedSegmentKind::Point { position: pos0, .. },
2340 UnsolvedSegmentKind::Point { position: pos1, .. },
2341 ) => {
2342 match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2345 (
2346 UnsolvedExpr::Unknown(p0_x),
2347 UnsolvedExpr::Unknown(p0_y),
2348 UnsolvedExpr::Unknown(p1_x),
2349 UnsolvedExpr::Unknown(p1_y),
2350 ) => {
2351 let sketch_constraint = SketchConstraint {
2353 kind: SketchConstraintKind::HorizontalDistance {
2354 points: [
2355 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2356 vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2357 object_id: unsolved0.object_id,
2358 }),
2359 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2360 vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2361 object_id: unsolved1.object_id,
2362 }),
2363 ],
2364 label_position,
2365 },
2366 meta: vec![args.source_range.into()],
2367 };
2368 Ok(KclValue::SketchConstraint {
2369 value: Box::new(sketch_constraint),
2370 })
2371 }
2372 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2373 "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
2374 .to_owned(),
2375 vec![args.source_range],
2376 ))),
2377 }
2378 }
2379 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2380 "horizontalDistance() arguments must be unsolved points".to_owned(),
2381 vec![args.source_range],
2382 ))),
2383 }
2384 }
2385 (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2387 if !point2d_is_origin(point2d) {
2388 return Err(KclError::new_semantic(KclErrorDetails::new(
2389 "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
2390 vec![args.source_range],
2391 )));
2392 }
2393
2394 let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2395 return Err(KclError::new_semantic(KclErrorDetails::new(
2396 "segment must be an unsolved segment".to_owned(),
2397 vec![args.source_range],
2398 )));
2399 };
2400 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2401 return Err(KclError::new_semantic(KclErrorDetails::new(
2402 "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2403 vec![args.source_range],
2404 )));
2405 };
2406 match (&position[0], &position[1]) {
2407 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2408 let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2409 vars: crate::front::Point2d {
2410 x: *point_x,
2411 y: *point_y,
2412 },
2413 object_id: unsolved.object_id,
2414 });
2415 let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2416 [point, ConstrainablePoint2dOrOrigin::Origin]
2417 } else {
2418 [ConstrainablePoint2dOrOrigin::Origin, point]
2419 };
2420 Ok(KclValue::SketchConstraint {
2421 value: Box::new(SketchConstraint {
2422 kind: SketchConstraintKind::HorizontalDistance { points, label_position },
2423 meta: vec![args.source_range.into()],
2424 }),
2425 })
2426 }
2427 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2428 "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
2429 .to_owned(),
2430 vec![args.source_range],
2431 ))),
2432 }
2433 }
2434 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2435 "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
2436 vec![args.source_range],
2437 ))),
2438 }
2439}
2440
2441pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2442 let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2443 "points",
2444 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2445 exec_state,
2446 )?;
2447 let label_position = get_constraint_label_position(exec_state, &args, "verticalDistance")?;
2448 let [p1, p2] = points.as_slice() else {
2449 return Err(KclError::new_semantic(KclErrorDetails::new(
2450 "must have two input points".to_owned(),
2451 vec![args.source_range],
2452 )));
2453 };
2454 match (p1, p2) {
2455 (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2456 let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2457 return Err(KclError::new_semantic(KclErrorDetails::new(
2458 "first point must be an unsolved segment".to_owned(),
2459 vec![args.source_range],
2460 )));
2461 };
2462 let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2463 return Err(KclError::new_semantic(KclErrorDetails::new(
2464 "second point must be an unsolved segment".to_owned(),
2465 vec![args.source_range],
2466 )));
2467 };
2468 match (&unsolved0.kind, &unsolved1.kind) {
2469 (
2470 UnsolvedSegmentKind::Point { position: pos0, .. },
2471 UnsolvedSegmentKind::Point { position: pos1, .. },
2472 ) => {
2473 match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2476 (
2477 UnsolvedExpr::Unknown(p0_x),
2478 UnsolvedExpr::Unknown(p0_y),
2479 UnsolvedExpr::Unknown(p1_x),
2480 UnsolvedExpr::Unknown(p1_y),
2481 ) => {
2482 let sketch_constraint = SketchConstraint {
2484 kind: SketchConstraintKind::VerticalDistance {
2485 points: [
2486 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2487 vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2488 object_id: unsolved0.object_id,
2489 }),
2490 ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2491 vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2492 object_id: unsolved1.object_id,
2493 }),
2494 ],
2495 label_position,
2496 },
2497 meta: vec![args.source_range.into()],
2498 };
2499 Ok(KclValue::SketchConstraint {
2500 value: Box::new(sketch_constraint),
2501 })
2502 }
2503 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2504 "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
2505 .to_owned(),
2506 vec![args.source_range],
2507 ))),
2508 }
2509 }
2510 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2511 "verticalDistance() arguments must be unsolved points".to_owned(),
2512 vec![args.source_range],
2513 ))),
2514 }
2515 }
2516 (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2517 if !point2d_is_origin(point2d) {
2518 return Err(KclError::new_semantic(KclErrorDetails::new(
2519 "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
2520 vec![args.source_range],
2521 )));
2522 }
2523
2524 let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2525 return Err(KclError::new_semantic(KclErrorDetails::new(
2526 "segment must be an unsolved segment".to_owned(),
2527 vec![args.source_range],
2528 )));
2529 };
2530 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2531 return Err(KclError::new_semantic(KclErrorDetails::new(
2532 "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2533 vec![args.source_range],
2534 )));
2535 };
2536 match (&position[0], &position[1]) {
2537 (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2538 let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2539 vars: crate::front::Point2d {
2540 x: *point_x,
2541 y: *point_y,
2542 },
2543 object_id: unsolved.object_id,
2544 });
2545 let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2546 [point, ConstrainablePoint2dOrOrigin::Origin]
2547 } else {
2548 [ConstrainablePoint2dOrOrigin::Origin, point]
2549 };
2550 Ok(KclValue::SketchConstraint {
2551 value: Box::new(SketchConstraint {
2552 kind: SketchConstraintKind::VerticalDistance { points, label_position },
2553 meta: vec![args.source_range.into()],
2554 }),
2555 })
2556 }
2557 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2558 "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
2559 .to_owned(),
2560 vec![args.source_range],
2561 ))),
2562 }
2563 }
2564 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2565 "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
2566 vec![args.source_range],
2567 ))),
2568 }
2569}
2570
2571#[derive(Debug, Clone, Copy)]
2572struct MidpointPointVars {
2573 coords: [SketchVarId; 2],
2574 #[cfg(feature = "artifact-graph")]
2575 object_id: ObjectId,
2576}
2577
2578#[derive(Debug, Clone, Copy)]
2579enum MidpointTargetVars {
2580 Line {
2581 start: [SketchVarId; 2],
2582 end: [SketchVarId; 2],
2583 #[cfg(feature = "artifact-graph")]
2584 object_id: ObjectId,
2585 },
2586 Arc {
2587 center: [SketchVarId; 2],
2588 start: [SketchVarId; 2],
2589 end: [SketchVarId; 2],
2590 #[cfg(feature = "artifact-graph")]
2591 object_id: ObjectId,
2592 },
2593}
2594
2595impl MidpointTargetVars {
2596 #[cfg(feature = "artifact-graph")]
2597 fn object_id(self) -> ObjectId {
2598 match self {
2599 Self::Line { object_id, .. } | Self::Arc { object_id, .. } => object_id,
2600 }
2601 }
2602}
2603
2604fn extract_midpoint_point(segment_value: &KclValue, range: crate::SourceRange) -> Result<MidpointPointVars, KclError> {
2605 let KclValue::Segment { value: segment } = segment_value else {
2606 return Err(KclError::new_semantic(KclErrorDetails::new(
2607 format!(
2608 "midpoint() point must be a point Segment, but found {}",
2609 segment_value.human_friendly_type()
2610 ),
2611 vec![range],
2612 )));
2613 };
2614 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2615 return Err(KclError::new_semantic(KclErrorDetails::new(
2616 "midpoint() point must be an unsolved point Segment".to_owned(),
2617 vec![range],
2618 )));
2619 };
2620 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2621 return Err(KclError::new_semantic(KclErrorDetails::new(
2622 "midpoint() point must be a point Segment".to_owned(),
2623 vec![range],
2624 )));
2625 };
2626 let (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) = (&position[0], &position[1]) else {
2627 return Err(KclError::new_semantic(KclErrorDetails::new(
2628 "midpoint() point coordinates must be sketch vars".to_owned(),
2629 vec![range],
2630 )));
2631 };
2632
2633 Ok(MidpointPointVars {
2634 coords: [*point_x, *point_y],
2635 #[cfg(feature = "artifact-graph")]
2636 object_id: unsolved.object_id,
2637 })
2638}
2639
2640fn extract_midpoint_target(
2641 segment_value: &KclValue,
2642 range: crate::SourceRange,
2643) -> Result<MidpointTargetVars, KclError> {
2644 let KclValue::Segment { value: segment } = segment_value else {
2645 return Err(KclError::new_semantic(KclErrorDetails::new(
2646 format!(
2647 "midpoint() target must be a line or arc Segment, but found {}",
2648 segment_value.human_friendly_type()
2649 ),
2650 vec![range],
2651 )));
2652 };
2653 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2654 return Err(KclError::new_semantic(KclErrorDetails::new(
2655 "midpoint() target must be an unsolved line or arc Segment".to_owned(),
2656 vec![range],
2657 )));
2658 };
2659 match &unsolved.kind {
2660 UnsolvedSegmentKind::Line { start, end, .. } => {
2661 let (
2662 UnsolvedExpr::Unknown(start_x),
2663 UnsolvedExpr::Unknown(start_y),
2664 UnsolvedExpr::Unknown(end_x),
2665 UnsolvedExpr::Unknown(end_y),
2666 ) = (&start[0], &start[1], &end[0], &end[1])
2667 else {
2668 return Err(KclError::new_semantic(KclErrorDetails::new(
2669 "midpoint() line coordinates must be sketch vars".to_owned(),
2670 vec![range],
2671 )));
2672 };
2673
2674 Ok(MidpointTargetVars::Line {
2675 start: [*start_x, *start_y],
2676 end: [*end_x, *end_y],
2677 #[cfg(feature = "artifact-graph")]
2678 object_id: unsolved.object_id,
2679 })
2680 }
2681 UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2682 let (
2683 UnsolvedExpr::Unknown(center_x),
2684 UnsolvedExpr::Unknown(center_y),
2685 UnsolvedExpr::Unknown(start_x),
2686 UnsolvedExpr::Unknown(start_y),
2687 UnsolvedExpr::Unknown(end_x),
2688 UnsolvedExpr::Unknown(end_y),
2689 ) = (¢er[0], ¢er[1], &start[0], &start[1], &end[0], &end[1])
2690 else {
2691 return Err(KclError::new_semantic(KclErrorDetails::new(
2692 "midpoint() arc center/start/end coordinates must be sketch vars".to_owned(),
2693 vec![range],
2694 )));
2695 };
2696
2697 Ok(MidpointTargetVars::Arc {
2698 center: [*center_x, *center_y],
2699 start: [*start_x, *start_y],
2700 end: [*end_x, *end_y],
2701 #[cfg(feature = "artifact-graph")]
2702 object_id: unsolved.object_id,
2703 })
2704 }
2705 _ => Err(KclError::new_semantic(KclErrorDetails::new(
2706 "midpoint() target must be a line or circular arc Segment".to_owned(),
2707 vec![range],
2708 ))),
2709 }
2710}
2711
2712pub async fn midpoint(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2713 let target: KclValue =
2714 args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
2715 let point: KclValue = args.get_kw_arg("point", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
2716 let range = args.source_range;
2717
2718 let point = extract_midpoint_point(&point, range)?;
2719 let target = extract_midpoint_target(&target, range)?;
2720
2721 #[cfg(feature = "artifact-graph")]
2722 let constraint_id = exec_state.next_object_id();
2723 let Some(sketch_state) = exec_state.sketch_block_mut() else {
2724 return Err(KclError::new_semantic(KclErrorDetails::new(
2725 "midpoint() can only be used inside a sketch block".to_owned(),
2726 vec![range],
2727 )));
2728 };
2729
2730 let solver_point = datum_point(point.coords, range)?;
2731 match target {
2732 MidpointTargetVars::Line { start, end, .. } => {
2733 sketch_state.solver_constraints.push(SolverConstraint::Midpoint(
2734 DatumLineSegment::new(datum_point(start, range)?, datum_point(end, range)?),
2735 solver_point,
2736 ));
2737 }
2738 MidpointTargetVars::Arc { center, start, end, .. } => {
2739 sketch_state
2740 .solver_constraints
2741 .extend(SolverConstraint::point_bisects_arc(
2742 DatumCircularArc {
2743 center: datum_point(center, range)?,
2744 start: datum_point(start, range)?,
2745 end: datum_point(end, range)?,
2746 },
2747 solver_point,
2748 ));
2749 }
2750 }
2751
2752 #[cfg(feature = "artifact-graph")]
2753 {
2754 let constraint = Constraint::Midpoint(Midpoint {
2755 point: point.object_id,
2756 segment: target.object_id(),
2757 });
2758 sketch_state.sketch_constraints.push(constraint_id);
2759 track_constraint(constraint_id, constraint, exec_state, &args);
2760 }
2761
2762 Ok(KclValue::none())
2763}
2764
2765pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2766 #[derive(Clone, Copy)]
2767 struct ConstrainableLine {
2768 solver_line: DatumLineSegment,
2769 #[cfg(feature = "artifact-graph")]
2770 object_id: ObjectId,
2771 }
2772
2773 let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2774 "lines",
2775 &RuntimeType::Array(
2776 Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2777 ArrayLen::Minimum(2),
2778 ),
2779 exec_state,
2780 )?;
2781 let range = args.source_range;
2782 let constrainable_lines: Vec<ConstrainableLine> = lines
2783 .iter()
2784 .map(|line| {
2785 let KclValue::Segment { value: segment } = line else {
2786 return Err(KclError::new_semantic(KclErrorDetails::new(
2787 "line argument must be a Segment".to_owned(),
2788 vec![args.source_range],
2789 )));
2790 };
2791 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2792 return Err(KclError::new_internal(KclErrorDetails::new(
2793 "line must be an unsolved Segment".to_owned(),
2794 vec![args.source_range],
2795 )));
2796 };
2797 let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2798 return Err(KclError::new_semantic(KclErrorDetails::new(
2799 "line argument must be a line, no other type of Segment".to_owned(),
2800 vec![args.source_range],
2801 )));
2802 };
2803 let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2804 return Err(KclError::new_semantic(KclErrorDetails::new(
2805 "line's start x coordinate must be a var".to_owned(),
2806 vec![args.source_range],
2807 )));
2808 };
2809 let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2810 return Err(KclError::new_semantic(KclErrorDetails::new(
2811 "line's start y coordinate must be a var".to_owned(),
2812 vec![args.source_range],
2813 )));
2814 };
2815 let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2816 return Err(KclError::new_semantic(KclErrorDetails::new(
2817 "line's end x coordinate must be a var".to_owned(),
2818 vec![args.source_range],
2819 )));
2820 };
2821 let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2822 return Err(KclError::new_semantic(KclErrorDetails::new(
2823 "line's end y coordinate must be a var".to_owned(),
2824 vec![args.source_range],
2825 )));
2826 };
2827
2828 let solver_line_p0 =
2829 DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2830 let solver_line_p1 =
2831 DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2832
2833 Ok(ConstrainableLine {
2834 solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2835 #[cfg(feature = "artifact-graph")]
2836 object_id: unsolved.object_id,
2837 })
2838 })
2839 .collect::<Result<_, _>>()?;
2840
2841 #[cfg(feature = "artifact-graph")]
2842 let constraint_id = exec_state.next_object_id();
2843 let Some(sketch_state) = exec_state.sketch_block_mut() else {
2845 return Err(KclError::new_semantic(KclErrorDetails::new(
2846 "equalLength() can only be used inside a sketch block".to_owned(),
2847 vec![args.source_range],
2848 )));
2849 };
2850 let first_line = constrainable_lines[0];
2851 for line in constrainable_lines.iter().skip(1) {
2852 sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
2853 first_line.solver_line,
2854 line.solver_line,
2855 ));
2856 }
2857 #[cfg(feature = "artifact-graph")]
2858 {
2859 let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
2860 lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
2861 });
2862 sketch_state.sketch_constraints.push(constraint_id);
2863 track_constraint(constraint_id, constraint, exec_state, &args);
2864 }
2865 Ok(KclValue::none())
2866}
2867
2868fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
2869 Ok(DatumPoint::new_xy(
2870 coords[0].to_constraint_id(range)?,
2871 coords[1].to_constraint_id(range)?,
2872 ))
2873}
2874
2875fn sketch_var_initial_value(
2876 sketch_vars: &[KclValue],
2877 id: SketchVarId,
2878 exec_state: &mut ExecState,
2879 range: crate::SourceRange,
2880) -> Result<f64, KclError> {
2881 sketch_vars
2882 .get(id.0)
2883 .and_then(KclValue::as_sketch_var)
2884 .map(|sketch_var| {
2885 sketch_var
2886 .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
2887 .map(|value| value.n)
2888 })
2889 .transpose()?
2890 .ok_or_else(|| {
2891 KclError::new_internal(KclErrorDetails::new(
2892 format!("Missing sketch variable initial value for id {}", id.0),
2893 vec![range],
2894 ))
2895 })
2896}
2897
2898fn radius_guess(
2899 sketch_vars: &[KclValue],
2900 center: [SketchVarId; 2],
2901 point: [SketchVarId; 2],
2902 exec_state: &mut ExecState,
2903 range: crate::SourceRange,
2904) -> Result<f64, KclError> {
2905 let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
2906 - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
2907 let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
2908 - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
2909 Ok(libm::hypot(dx, dy))
2910}
2911
2912fn reflect_point_across_line(point: [f64; 2], axis_start: [f64; 2], axis_end: [f64; 2]) -> [f64; 2] {
2913 let [px, py] = point;
2914 let [ax, ay] = axis_start;
2915 let [bx, by] = axis_end;
2916 let dx = bx - ax;
2917 let dy = by - ay;
2918 let axis_len_sq = dx * dx + dy * dy;
2919 if axis_len_sq <= f64::EPSILON {
2920 return point;
2921 }
2922
2923 let point_from_axis = [px - ax, py - ay];
2924 let projection_scale = (point_from_axis[0] * dx + point_from_axis[1] * dy) / axis_len_sq;
2925 let projected = [ax + projection_scale * dx, ay + projection_scale * dy];
2926
2927 [2.0 * projected[0] - px, 2.0 * projected[1] - py]
2928}
2929
2930fn symmetric_hidden_point_guess(
2933 sketch_vars: &[KclValue],
2934 point: [SketchVarId; 2],
2935 axis: SymmetricLineVars,
2936 exec_state: &mut ExecState,
2937 range: crate::SourceRange,
2938) -> Result<[f64; 2], KclError> {
2939 let point = [
2940 sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
2941 sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
2942 ];
2943 let axis_start = [
2944 sketch_var_initial_value(sketch_vars, axis.start[0], exec_state, range)?,
2945 sketch_var_initial_value(sketch_vars, axis.start[1], exec_state, range)?,
2946 ];
2947 let axis_end = [
2948 sketch_var_initial_value(sketch_vars, axis.end[0], exec_state, range)?,
2949 sketch_var_initial_value(sketch_vars, axis.end[1], exec_state, range)?,
2950 ];
2951
2952 Ok(reflect_point_across_line(point, axis_start, axis_end))
2953}
2954
2955fn create_hidden_point(
2956 exec_state: &mut ExecState,
2957 initial_position: [f64; 2],
2958 range: crate::SourceRange,
2959) -> Result<[SketchVarId; 2], KclError> {
2960 let sketch_var_ty = solver_numeric_type(exec_state);
2961 let Some(sketch_state) = exec_state.sketch_block_mut() else {
2962 return Err(KclError::new_semantic(KclErrorDetails::new(
2963 "symmetric() can only be used inside a sketch block".to_owned(),
2964 vec![range],
2965 )));
2966 };
2967
2968 let x_id = sketch_state.next_sketch_var_id();
2969 sketch_state.sketch_vars.push(KclValue::SketchVar {
2970 value: Box::new(crate::execution::SketchVar {
2971 id: x_id,
2972 initial_value: initial_position[0],
2973 ty: sketch_var_ty,
2974 meta: vec![],
2975 }),
2976 });
2977
2978 let y_id = sketch_state.next_sketch_var_id();
2979 sketch_state.sketch_vars.push(KclValue::SketchVar {
2980 value: Box::new(crate::execution::SketchVar {
2981 id: y_id,
2982 initial_value: initial_position[1],
2983 ty: sketch_var_ty,
2984 meta: vec![],
2985 }),
2986 });
2987
2988 Ok([x_id, y_id])
2989}
2990
2991pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2992 #[derive(Debug, Clone, Copy)]
2993 struct RadiusInputVars {
2994 center: [SketchVarId; 2],
2995 start: [SketchVarId; 2],
2996 end: Option<[SketchVarId; 2]>,
2997 }
2998
2999 #[derive(Debug, Clone, Copy)]
3000 enum EqualRadiusInput {
3001 Radius(RadiusInputVars),
3002 }
3003
3004 fn extract_equal_radius_input(
3005 segment_value: &KclValue,
3006 range: crate::SourceRange,
3007 ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
3008 let KclValue::Segment { value: segment } = segment_value else {
3009 return Err(KclError::new_semantic(KclErrorDetails::new(
3010 format!(
3011 "equalRadius() arguments must be segments but found {}",
3012 segment_value.human_friendly_type()
3013 ),
3014 vec![range],
3015 )));
3016 };
3017 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3018 return Err(KclError::new_semantic(KclErrorDetails::new(
3019 "equalRadius() arguments must be unsolved segments".to_owned(),
3020 vec![range],
3021 )));
3022 };
3023 match &unsolved.kind {
3024 UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3025 let (
3026 UnsolvedExpr::Unknown(center_x),
3027 UnsolvedExpr::Unknown(center_y),
3028 UnsolvedExpr::Unknown(start_x),
3029 UnsolvedExpr::Unknown(start_y),
3030 UnsolvedExpr::Unknown(end_x),
3031 UnsolvedExpr::Unknown(end_y),
3032 ) = (¢er[0], ¢er[1], &start[0], &start[1], &end[0], &end[1])
3033 else {
3034 return Err(KclError::new_semantic(KclErrorDetails::new(
3035 "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
3036 vec![range],
3037 )));
3038 };
3039 Ok((
3040 EqualRadiusInput::Radius(RadiusInputVars {
3041 center: [*center_x, *center_y],
3042 start: [*start_x, *start_y],
3043 end: Some([*end_x, *end_y]),
3044 }),
3045 unsolved.object_id,
3046 ))
3047 }
3048 UnsolvedSegmentKind::Circle { center, start, .. } => {
3049 let (
3050 UnsolvedExpr::Unknown(center_x),
3051 UnsolvedExpr::Unknown(center_y),
3052 UnsolvedExpr::Unknown(start_x),
3053 UnsolvedExpr::Unknown(start_y),
3054 ) = (¢er[0], ¢er[1], &start[0], &start[1])
3055 else {
3056 return Err(KclError::new_semantic(KclErrorDetails::new(
3057 "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
3058 vec![range],
3059 )));
3060 };
3061 Ok((
3062 EqualRadiusInput::Radius(RadiusInputVars {
3063 center: [*center_x, *center_y],
3064 start: [*start_x, *start_y],
3065 end: None,
3066 }),
3067 unsolved.object_id,
3068 ))
3069 }
3070 other => Err(KclError::new_semantic(KclErrorDetails::new(
3071 format!(
3072 "equalRadius() currently supports only arc and circle segments, you provided {}",
3073 other.human_friendly_kind_with_article()
3074 ),
3075 vec![range],
3076 ))),
3077 }
3078 }
3079
3080 let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3081 "input",
3082 &RuntimeType::Array(
3083 Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3084 ArrayLen::Minimum(2),
3085 ),
3086 exec_state,
3087 )?;
3088 let range = args.source_range;
3089
3090 let extracted_input = input
3091 .iter()
3092 .map(|segment_value| extract_equal_radius_input(segment_value, range))
3093 .collect::<Result<Vec<_>, _>>()?;
3094 let radius_inputs: Vec<RadiusInputVars> = extracted_input
3095 .iter()
3096 .map(|(equal_radius_input, _)| match equal_radius_input {
3097 EqualRadiusInput::Radius(radius_input) => *radius_input,
3098 })
3099 .collect();
3100 #[cfg(feature = "artifact-graph")]
3101 let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
3102
3103 let sketch_var_ty = solver_numeric_type(exec_state);
3104 #[cfg(feature = "artifact-graph")]
3105 let constraint_id = exec_state.next_object_id();
3106
3107 let sketch_vars = {
3108 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3109 return Err(KclError::new_semantic(KclErrorDetails::new(
3110 "equalRadius() can only be used inside a sketch block".to_owned(),
3111 vec![range],
3112 )));
3113 };
3114 sketch_state.sketch_vars.clone()
3115 };
3116
3117 let radius_initial_value = radius_guess(
3118 &sketch_vars,
3119 radius_inputs[0].center,
3120 radius_inputs[0].start,
3121 exec_state,
3122 range,
3123 )?;
3124
3125 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3126 return Err(KclError::new_semantic(KclErrorDetails::new(
3127 "equalRadius() can only be used inside a sketch block".to_owned(),
3128 vec![range],
3129 )));
3130 };
3131 let radius_id = sketch_state.next_sketch_var_id();
3132 sketch_state.sketch_vars.push(KclValue::SketchVar {
3133 value: Box::new(crate::execution::SketchVar {
3134 id: radius_id,
3135 initial_value: radius_initial_value,
3136 ty: sketch_var_ty,
3137 meta: vec![],
3138 }),
3139 });
3140 let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3141
3142 for radius_input in radius_inputs {
3143 let center = datum_point(radius_input.center, range)?;
3144 let start = datum_point(radius_input.start, range)?;
3145 sketch_state
3146 .solver_constraints
3147 .push(SolverConstraint::DistanceVar(start, center, radius));
3148 if let Some(end) = radius_input.end {
3149 let end = datum_point(end, range)?;
3150 sketch_state
3151 .solver_constraints
3152 .push(SolverConstraint::DistanceVar(end, center, radius));
3153 }
3154 }
3155
3156 #[cfg(feature = "artifact-graph")]
3157 {
3158 let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
3159 input: input_object_ids,
3160 });
3161 sketch_state.sketch_constraints.push(constraint_id);
3162 track_constraint(constraint_id, constraint, exec_state, &args);
3163 }
3164
3165 Ok(KclValue::none())
3166}
3167
3168pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3169 let Some(Some(sketch_id)) = exec_state.sketch_block().map(|sb| sb.sketch_id) else {
3170 return Err(KclError::new_semantic(KclErrorDetails::new(
3171 "tangent() cannot be used outside a sketch block".to_owned(),
3172 vec![args.source_range],
3173 )));
3174 };
3175
3176 #[derive(Debug, Clone, Copy)]
3177 enum TangentInput {
3178 Line(LineVars),
3179 Circular(ArcVars),
3180 }
3181
3182 fn extract_tangent_input(
3183 segment_value: &KclValue,
3184 range: crate::SourceRange,
3185 ) -> Result<(TangentInput, ObjectId), KclError> {
3186 let KclValue::Segment { value: segment } = segment_value else {
3187 return Err(KclError::new_semantic(KclErrorDetails::new(
3188 "tangent() arguments must be segments".to_owned(),
3189 vec![range],
3190 )));
3191 };
3192 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3193 return Err(KclError::new_semantic(KclErrorDetails::new(
3194 "tangent() arguments must be unsolved segments".to_owned(),
3195 vec![range],
3196 )));
3197 };
3198 match &unsolved.kind {
3199 UnsolvedSegmentKind::Line { start, end, .. } => {
3200 let (
3201 UnsolvedExpr::Unknown(start_x),
3202 UnsolvedExpr::Unknown(start_y),
3203 UnsolvedExpr::Unknown(end_x),
3204 UnsolvedExpr::Unknown(end_y),
3205 ) = (&start[0], &start[1], &end[0], &end[1])
3206 else {
3207 return Err(KclError::new_semantic(KclErrorDetails::new(
3208 "line coordinates must be sketch vars for tangent()".to_owned(),
3209 vec![range],
3210 )));
3211 };
3212 Ok((
3213 TangentInput::Line(LineVars {
3214 start: [*start_x, *start_y],
3215 end: [*end_x, *end_y],
3216 }),
3217 unsolved.object_id,
3218 ))
3219 }
3220 UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3221 let (
3222 UnsolvedExpr::Unknown(center_x),
3223 UnsolvedExpr::Unknown(center_y),
3224 UnsolvedExpr::Unknown(start_x),
3225 UnsolvedExpr::Unknown(start_y),
3226 UnsolvedExpr::Unknown(end_x),
3227 UnsolvedExpr::Unknown(end_y),
3228 ) = (¢er[0], ¢er[1], &start[0], &start[1], &end[0], &end[1])
3229 else {
3230 return Err(KclError::new_semantic(KclErrorDetails::new(
3231 "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
3232 vec![range],
3233 )));
3234 };
3235 Ok((
3236 TangentInput::Circular(ArcVars {
3237 center: [*center_x, *center_y],
3238 start: [*start_x, *start_y],
3239 end: Some([*end_x, *end_y]),
3240 }),
3241 unsolved.object_id,
3242 ))
3243 }
3244 UnsolvedSegmentKind::Circle { center, start, .. } => {
3245 let (
3246 UnsolvedExpr::Unknown(center_x),
3247 UnsolvedExpr::Unknown(center_y),
3248 UnsolvedExpr::Unknown(start_x),
3249 UnsolvedExpr::Unknown(start_y),
3250 ) = (¢er[0], ¢er[1], &start[0], &start[1])
3251 else {
3252 return Err(KclError::new_semantic(KclErrorDetails::new(
3253 "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
3254 vec![range],
3255 )));
3256 };
3257 Ok((
3258 TangentInput::Circular(ArcVars {
3259 center: [*center_x, *center_y],
3260 start: [*start_x, *start_y],
3261 end: None,
3262 }),
3263 unsolved.object_id,
3264 ))
3265 }
3266 _ => Err(KclError::new_semantic(KclErrorDetails::new(
3267 "tangent() supports only line, arc, and circle segments".to_owned(),
3268 vec![range],
3269 ))),
3270 }
3271 }
3272
3273 let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3274 "input",
3275 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3276 exec_state,
3277 )?;
3278 let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
3279 KclError::new_semantic(KclErrorDetails::new(
3280 "tangent() requires exactly 2 input segments".to_owned(),
3281 vec![args.source_range],
3282 ))
3283 })?;
3284 let range = args.source_range;
3285 let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
3286 let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
3287 #[cfg(not(feature = "artifact-graph"))]
3288 let _ = (input0_object_id, input1_object_id);
3289
3290 enum TangentCase {
3291 LineCircular(LineVars, ArcVars),
3292 CircularCircular(ArcVars, ArcVars),
3293 }
3294 let tangent_case = match (input0, input1) {
3295 (TangentInput::Line(line), TangentInput::Circular(circular))
3296 | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
3297 (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
3298 TangentCase::CircularCircular(circular0, circular1)
3299 }
3300 (TangentInput::Line(_), TangentInput::Line(_)) => {
3301 return Err(KclError::new_semantic(KclErrorDetails::new(
3302 "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
3303 vec![range],
3304 )));
3305 }
3306 };
3307
3308 let sketch_var_ty = solver_numeric_type(exec_state);
3309 #[cfg(feature = "artifact-graph")]
3310 let constraint_id = exec_state.next_object_id();
3311
3312 let sketch_vars = {
3313 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3314 return Err(KclError::new_semantic(KclErrorDetails::new(
3315 "tangent() can only be used inside a sketch block".to_owned(),
3316 vec![range],
3317 )));
3318 };
3319 sketch_state.sketch_vars.clone()
3320 };
3321
3322 match tangent_case {
3324 TangentCase::LineCircular(line, circular) => {
3325 let tangency_key = make_line_arc_tangency_key(line, circular);
3326 let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
3327 Some(ConstraintState::Tangency(TangencyMode::LineCircle(side))) => side,
3328 _ => {
3329 let side = infer_line_tangent_side(&sketch_vars, line, circular.center, exec_state, range)?;
3330 exec_state.set_constraint_state(
3331 sketch_id,
3332 tangency_key,
3333 ConstraintState::Tangency(TangencyMode::LineCircle(side)),
3334 );
3335 side
3336 }
3337 };
3338 let line_p0 = datum_point(line.start, range)?;
3339 let line_p1 = datum_point(line.end, range)?;
3340 let line_datum = DatumLineSegment::new(line_p0, line_p1);
3341
3342 let center = datum_point(circular.center, range)?;
3343 let circular_start = datum_point(circular.start, range)?;
3344 let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
3345 let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
3346 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3347 return Err(KclError::new_semantic(KclErrorDetails::new(
3348 "tangent() can only be used inside a sketch block".to_owned(),
3349 vec![range],
3350 )));
3351 };
3352 let radius_id = sketch_state.next_sketch_var_id();
3353 sketch_state.sketch_vars.push(KclValue::SketchVar {
3354 value: Box::new(crate::execution::SketchVar {
3355 id: radius_id,
3356 initial_value: radius_initial_value,
3357 ty: sketch_var_ty,
3358 meta: vec![],
3359 }),
3360 });
3361 let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3362 let circle = DatumCircle { center, radius };
3363
3364 sketch_state
3369 .solver_constraints
3370 .push(SolverConstraint::DistanceVar(circular_start, center, radius));
3371 if let Some(circular_end) = circular_end {
3372 sketch_state
3373 .solver_constraints
3374 .push(SolverConstraint::DistanceVar(circular_end, center, radius));
3375 }
3376 sketch_state
3377 .solver_constraints
3378 .push(SolverConstraint::LineTangentToCircle(line_datum, circle, tangency_side));
3379 }
3380 TangentCase::CircularCircular(circular0, circular1) => {
3381 let tangency_key = make_arc_arc_tangency_key(circular0, circular1);
3382 let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
3383 Some(ConstraintState::Tangency(TangencyMode::CircleCircle(side))) => side,
3384 _ => {
3385 let side = infer_arc_tangent_side(&sketch_vars, circular0, circular1, exec_state, range)?;
3386 exec_state.set_constraint_state(
3387 sketch_id,
3388 tangency_key,
3389 ConstraintState::Tangency(TangencyMode::CircleCircle(side)),
3390 );
3391 side
3392 }
3393 };
3394 let center0 = datum_point(circular0.center, range)?;
3395 let start0 = datum_point(circular0.start, range)?;
3396 let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
3397 let radius0_initial_value =
3398 radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
3399 let center1 = datum_point(circular1.center, range)?;
3400 let start1 = datum_point(circular1.start, range)?;
3401 let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
3402 let radius1_initial_value =
3403 radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
3404 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3405 return Err(KclError::new_semantic(KclErrorDetails::new(
3406 "tangent() can only be used inside a sketch block".to_owned(),
3407 vec![range],
3408 )));
3409 };
3410 let radius0_id = sketch_state.next_sketch_var_id();
3411 sketch_state.sketch_vars.push(KclValue::SketchVar {
3412 value: Box::new(crate::execution::SketchVar {
3413 id: radius0_id,
3414 initial_value: radius0_initial_value,
3415 ty: sketch_var_ty,
3416 meta: vec![],
3417 }),
3418 });
3419 let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
3420 let circle0 = DatumCircle {
3421 center: center0,
3422 radius: radius0,
3423 };
3424
3425 let radius1_id = sketch_state.next_sketch_var_id();
3426 sketch_state.sketch_vars.push(KclValue::SketchVar {
3427 value: Box::new(crate::execution::SketchVar {
3428 id: radius1_id,
3429 initial_value: radius1_initial_value,
3430 ty: sketch_var_ty,
3431 meta: vec![],
3432 }),
3433 });
3434 let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
3435 let circle1 = DatumCircle {
3436 center: center1,
3437 radius: radius1,
3438 };
3439
3440 sketch_state
3445 .solver_constraints
3446 .push(SolverConstraint::DistanceVar(start0, center0, radius0));
3447 if let Some(end0) = end0 {
3448 sketch_state
3449 .solver_constraints
3450 .push(SolverConstraint::DistanceVar(end0, center0, radius0));
3451 }
3452 sketch_state
3453 .solver_constraints
3454 .push(SolverConstraint::DistanceVar(start1, center1, radius1));
3455 if let Some(end1) = end1 {
3456 sketch_state
3457 .solver_constraints
3458 .push(SolverConstraint::DistanceVar(end1, center1, radius1));
3459 }
3460 sketch_state
3461 .solver_constraints
3462 .push(SolverConstraint::CircleTangentToCircle(circle0, circle1, tangency_side));
3463 }
3464 }
3465
3466 #[cfg(feature = "artifact-graph")]
3467 {
3468 let constraint = crate::front::Constraint::Tangent(Tangent {
3469 input: vec![input0_object_id, input1_object_id],
3470 });
3471 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3472 return Err(KclError::new_semantic(KclErrorDetails::new(
3473 "tangent() can only be used inside a sketch block".to_owned(),
3474 vec![range],
3475 )));
3476 };
3477 sketch_state.sketch_constraints.push(constraint_id);
3478 track_constraint(constraint_id, constraint, exec_state, &args);
3479 }
3480
3481 Ok(KclValue::none())
3482}
3483
3484#[derive(Debug, Clone, Copy)]
3485struct SymmetricPointVars {
3486 coords: [SketchVarId; 2],
3487 #[cfg(feature = "artifact-graph")]
3488 object_id: ObjectId,
3489}
3490
3491#[derive(Debug, Clone, Copy)]
3493struct SymmetricLineVars {
3494 start: [SketchVarId; 2],
3495 end: [SketchVarId; 2],
3496 #[cfg(feature = "artifact-graph")]
3497 object_id: ObjectId,
3498}
3499
3500#[derive(Debug, Clone, Copy)]
3501struct SymmetricArcVars {
3502 center: [SketchVarId; 2],
3503 start: [SketchVarId; 2],
3504 end: [SketchVarId; 2],
3505 #[cfg(feature = "artifact-graph")]
3506 object_id: ObjectId,
3507}
3508
3509#[derive(Debug, Clone, Copy)]
3510struct SymmetricCircleVars {
3511 center: [SketchVarId; 2],
3512 start: [SketchVarId; 2],
3513 #[cfg(feature = "artifact-graph")]
3514 object_id: ObjectId,
3515}
3516
3517#[derive(Debug, Clone, Copy)]
3518enum SymmetricInput {
3519 Point(SymmetricPointVars),
3520 Line(SymmetricLineVars),
3521 Arc(SymmetricArcVars),
3522 Circle(SymmetricCircleVars),
3523}
3524
3525impl SymmetricInput {
3526 fn type_name(self) -> &'static str {
3527 match self {
3528 SymmetricInput::Point(_) => "points",
3529 SymmetricInput::Line(_) => "lines",
3530 SymmetricInput::Arc(_) => "arcs",
3531 SymmetricInput::Circle(_) => "circles",
3532 }
3533 }
3534
3535 #[cfg(feature = "artifact-graph")]
3536 fn object_id(self) -> ObjectId {
3537 match self {
3538 SymmetricInput::Point(point) => point.object_id,
3539 SymmetricInput::Line(line) => line.object_id,
3540 SymmetricInput::Arc(arc) => arc.object_id,
3541 SymmetricInput::Circle(circle) => circle.object_id,
3542 }
3543 }
3544}
3545
3546fn extract_symmetric_input(segment_value: &KclValue, range: crate::SourceRange) -> Result<SymmetricInput, KclError> {
3547 let KclValue::Segment { value: segment } = segment_value else {
3548 return Err(KclError::new_semantic(KclErrorDetails::new(
3549 format!(
3550 "symmetric() arguments must be point, line, arc, or circle segments, but found {}",
3551 segment_value.human_friendly_type()
3552 ),
3553 vec![range],
3554 )));
3555 };
3556 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3557 return Err(KclError::new_semantic(KclErrorDetails::new(
3558 "symmetric() arguments must be unsolved segments".to_owned(),
3559 vec![range],
3560 )));
3561 };
3562
3563 match &unsolved.kind {
3564 UnsolvedSegmentKind::Point { position, .. } => {
3565 let (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) = (&position[0], &position[1]) else {
3566 return Err(KclError::new_semantic(KclErrorDetails::new(
3567 "point coordinates must be sketch vars for symmetric()".to_owned(),
3568 vec![range],
3569 )));
3570 };
3571 Ok(SymmetricInput::Point(SymmetricPointVars {
3572 coords: [*x, *y],
3573 #[cfg(feature = "artifact-graph")]
3574 object_id: unsolved.object_id,
3575 }))
3576 }
3577 UnsolvedSegmentKind::Line { start, end, .. } => {
3578 let (
3579 UnsolvedExpr::Unknown(start_x),
3580 UnsolvedExpr::Unknown(start_y),
3581 UnsolvedExpr::Unknown(end_x),
3582 UnsolvedExpr::Unknown(end_y),
3583 ) = (&start[0], &start[1], &end[0], &end[1])
3584 else {
3585 return Err(KclError::new_semantic(KclErrorDetails::new(
3586 "line coordinates must be sketch vars for symmetric()".to_owned(),
3587 vec![range],
3588 )));
3589 };
3590 Ok(SymmetricInput::Line(SymmetricLineVars {
3591 start: [*start_x, *start_y],
3592 end: [*end_x, *end_y],
3593 #[cfg(feature = "artifact-graph")]
3594 object_id: unsolved.object_id,
3595 }))
3596 }
3597 UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3598 let (
3599 UnsolvedExpr::Unknown(center_x),
3600 UnsolvedExpr::Unknown(center_y),
3601 UnsolvedExpr::Unknown(start_x),
3602 UnsolvedExpr::Unknown(start_y),
3603 UnsolvedExpr::Unknown(end_x),
3604 UnsolvedExpr::Unknown(end_y),
3605 ) = (¢er[0], ¢er[1], &start[0], &start[1], &end[0], &end[1])
3606 else {
3607 return Err(KclError::new_semantic(KclErrorDetails::new(
3608 "arc center/start/end coordinates must be sketch vars for symmetric()".to_owned(),
3609 vec![range],
3610 )));
3611 };
3612 Ok(SymmetricInput::Arc(SymmetricArcVars {
3613 center: [*center_x, *center_y],
3614 start: [*start_x, *start_y],
3615 end: [*end_x, *end_y],
3616 #[cfg(feature = "artifact-graph")]
3617 object_id: unsolved.object_id,
3618 }))
3619 }
3620 UnsolvedSegmentKind::Circle { center, start, .. } => {
3621 let (
3622 UnsolvedExpr::Unknown(center_x),
3623 UnsolvedExpr::Unknown(center_y),
3624 UnsolvedExpr::Unknown(start_x),
3625 UnsolvedExpr::Unknown(start_y),
3626 ) = (¢er[0], ¢er[1], &start[0], &start[1])
3627 else {
3628 return Err(KclError::new_semantic(KclErrorDetails::new(
3629 "circle center/start coordinates must be sketch vars for symmetric()".to_owned(),
3630 vec![range],
3631 )));
3632 };
3633 Ok(SymmetricInput::Circle(SymmetricCircleVars {
3634 center: [*center_x, *center_y],
3635 start: [*start_x, *start_y],
3636 #[cfg(feature = "artifact-graph")]
3637 object_id: unsolved.object_id,
3638 }))
3639 }
3640 }
3641}
3642
3643fn extract_symmetric_axis_line(
3644 segment_value: &KclValue,
3645 range: crate::SourceRange,
3646) -> Result<SymmetricLineVars, KclError> {
3647 let KclValue::Segment { value: segment } = segment_value else {
3648 return Err(KclError::new_semantic(KclErrorDetails::new(
3649 format!(
3650 "symmetric() axis must be a line Segment, but found {}",
3651 segment_value.human_friendly_type()
3652 ),
3653 vec![range],
3654 )));
3655 };
3656 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3657 return Err(KclError::new_semantic(KclErrorDetails::new(
3658 "symmetric() axis must be an unsolved line Segment".to_owned(),
3659 vec![range],
3660 )));
3661 };
3662 let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3663 return Err(KclError::new_semantic(KclErrorDetails::new(
3664 "symmetric() axis must be a line Segment".to_owned(),
3665 vec![range],
3666 )));
3667 };
3668 let (
3669 UnsolvedExpr::Unknown(start_x),
3670 UnsolvedExpr::Unknown(start_y),
3671 UnsolvedExpr::Unknown(end_x),
3672 UnsolvedExpr::Unknown(end_y),
3673 ) = (&start[0], &start[1], &end[0], &end[1])
3674 else {
3675 return Err(KclError::new_semantic(KclErrorDetails::new(
3676 "symmetric() axis line coordinates must be sketch vars".to_owned(),
3677 vec![range],
3678 )));
3679 };
3680
3681 Ok(SymmetricLineVars {
3682 start: [*start_x, *start_y],
3683 end: [*end_x, *end_y],
3684 #[cfg(feature = "artifact-graph")]
3685 object_id: unsolved.object_id,
3686 })
3687}
3688
3689pub async fn symmetric(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3690 #[derive(Debug, Clone, Copy)]
3691 struct SymmetricCircularVars {
3692 center: [SketchVarId; 2],
3693 start: [SketchVarId; 2],
3694 end: Option<[SketchVarId; 2]>,
3695 }
3696
3697 let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3698 "input",
3699 &RuntimeType::Array(
3700 Box::new(RuntimeType::Primitive(PrimitiveType::Segment)),
3701 ArrayLen::Known(2),
3702 ),
3703 exec_state,
3704 )?;
3705 let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
3706 KclError::new_semantic(KclErrorDetails::new(
3707 "symmetric() requires exactly 2 input segments".to_owned(),
3708 vec![args.source_range],
3709 ))
3710 })?;
3711 let axis: KclValue = args.get_kw_arg("axis", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
3712 let range = args.source_range;
3713
3714 let input0 = extract_symmetric_input(&item0, range)?;
3715 let input1 = extract_symmetric_input(&item1, range)?;
3716 let axis_line = extract_symmetric_axis_line(&axis, range)?;
3717
3718 let solver_axis = DatumLineSegment::new(datum_point(axis_line.start, range)?, datum_point(axis_line.end, range)?);
3719
3720 let (mut solver_constraints, circular_inputs) = match (input0, input1) {
3721 (SymmetricInput::Point(point0), SymmetricInput::Point(point1)) => (
3722 vec![SolverConstraint::Symmetric(
3723 solver_axis,
3724 datum_point(point0.coords, range)?,
3725 datum_point(point1.coords, range)?,
3726 )],
3727 None,
3728 ),
3729 (SymmetricInput::Line(line0), SymmetricInput::Line(line1)) => {
3730 let sketch_vars = {
3731 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3732 return Err(KclError::new_semantic(KclErrorDetails::new(
3733 "symmetric() can only be used inside a sketch block".to_owned(),
3734 vec![range],
3735 )));
3736 };
3737 sketch_state.sketch_vars.clone()
3738 };
3739 let mirrored_start = symmetric_hidden_point_guess(&sketch_vars, line0.start, axis_line, exec_state, range)?;
3740 let mirrored_end = symmetric_hidden_point_guess(&sketch_vars, line0.end, axis_line, exec_state, range)?;
3741 let hidden_start = create_hidden_point(exec_state, mirrored_start, range)?;
3742 let hidden_end = create_hidden_point(exec_state, mirrored_end, range)?;
3743 let mirrored_support_line =
3744 DatumLineSegment::new(datum_point(hidden_start, range)?, datum_point(hidden_end, range)?);
3745 let solver_line1 = DatumLineSegment::new(datum_point(line1.start, range)?, datum_point(line1.end, range)?);
3746
3747 (
3748 vec![
3749 SolverConstraint::Symmetric(
3750 solver_axis,
3751 datum_point(line0.start, range)?,
3752 datum_point(hidden_start, range)?,
3753 ),
3754 SolverConstraint::Symmetric(
3755 solver_axis,
3756 datum_point(line0.end, range)?,
3757 datum_point(hidden_end, range)?,
3758 ),
3759 SolverConstraint::LinesAtAngle(mirrored_support_line, solver_line1, AngleKind::Parallel),
3760 SolverConstraint::PointLineDistance(datum_point(line1.start, range)?, mirrored_support_line, 0.0),
3763 ],
3764 None,
3765 )
3766 }
3767 (SymmetricInput::Arc(arc0), SymmetricInput::Arc(arc1)) => (
3768 vec![SolverConstraint::Symmetric(
3769 solver_axis,
3770 datum_point(arc0.center, range)?,
3771 datum_point(arc1.center, range)?,
3772 )],
3773 Some([
3774 SymmetricCircularVars {
3775 center: arc0.center,
3776 start: arc0.start,
3777 end: Some(arc0.end),
3778 },
3779 SymmetricCircularVars {
3780 center: arc1.center,
3781 start: arc1.start,
3782 end: Some(arc1.end),
3783 },
3784 ]),
3785 ),
3786 (SymmetricInput::Circle(circle0), SymmetricInput::Circle(circle1)) => (
3787 vec![SolverConstraint::Symmetric(
3788 solver_axis,
3789 datum_point(circle0.center, range)?,
3790 datum_point(circle1.center, range)?,
3791 )],
3792 Some([
3793 SymmetricCircularVars {
3794 center: circle0.center,
3795 start: circle0.start,
3796 end: None,
3797 },
3798 SymmetricCircularVars {
3799 center: circle1.center,
3800 start: circle1.start,
3801 end: None,
3802 },
3803 ]),
3804 ),
3805 _ => {
3806 return Err(KclError::new_semantic(KclErrorDetails::new(
3807 format!(
3808 "symmetric() inputs must be homogeneous. You provided {} and {}",
3809 input0.type_name(),
3810 input1.type_name()
3811 ),
3812 vec![range],
3813 )));
3814 }
3815 };
3816
3817 if let Some([circular0, circular1]) = circular_inputs {
3818 let sketch_var_ty = solver_numeric_type(exec_state);
3819 let sketch_vars = {
3820 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3821 return Err(KclError::new_semantic(KclErrorDetails::new(
3822 "symmetric() can only be used inside a sketch block".to_owned(),
3823 vec![range],
3824 )));
3825 };
3826 sketch_state.sketch_vars.clone()
3827 };
3828 let radius_initial_value = radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
3829
3830 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3831 return Err(KclError::new_semantic(KclErrorDetails::new(
3832 "symmetric() can only be used inside a sketch block".to_owned(),
3833 vec![range],
3834 )));
3835 };
3836 let radius_id = sketch_state.next_sketch_var_id();
3837 sketch_state.sketch_vars.push(KclValue::SketchVar {
3838 value: Box::new(crate::execution::SketchVar {
3839 id: radius_id,
3840 initial_value: radius_initial_value,
3841 ty: sketch_var_ty,
3842 meta: vec![],
3843 }),
3844 });
3845 let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3846
3847 for circular in [circular0, circular1] {
3848 let center = datum_point(circular.center, range)?;
3849 let start = datum_point(circular.start, range)?;
3850 solver_constraints.push(SolverConstraint::DistanceVar(start, center, radius));
3851 if let Some(end) = circular.end {
3852 let end = datum_point(end, range)?;
3853 solver_constraints.push(SolverConstraint::DistanceVar(end, center, radius));
3854 }
3855 }
3856 }
3857
3858 #[cfg(feature = "artifact-graph")]
3859 let constraint_id = exec_state.next_object_id();
3860 let Some(sketch_state) = exec_state.sketch_block_mut() else {
3861 return Err(KclError::new_semantic(KclErrorDetails::new(
3862 "symmetric() can only be used inside a sketch block".to_owned(),
3863 vec![range],
3864 )));
3865 };
3866 sketch_state.solver_constraints.extend(solver_constraints);
3867
3868 #[cfg(feature = "artifact-graph")]
3869 {
3870 let constraint = crate::front::Constraint::Symmetric(Symmetric {
3871 input: vec![input0.object_id(), input1.object_id()],
3872 axis: axis_line.object_id,
3873 });
3874 sketch_state.sketch_constraints.push(constraint_id);
3875 track_constraint(constraint_id, constraint, exec_state, &args);
3876 }
3877
3878 Ok(KclValue::none())
3879}
3880
3881#[derive(Debug, Clone, Copy)]
3882pub(crate) enum LinesAtAngleKind {
3883 Parallel,
3884 Perpendicular,
3885}
3886
3887impl LinesAtAngleKind {
3888 pub fn to_function_name(self) -> &'static str {
3889 match self {
3890 LinesAtAngleKind::Parallel => "parallel",
3891 LinesAtAngleKind::Perpendicular => "perpendicular",
3892 }
3893 }
3894
3895 fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
3896 match self {
3897 LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
3898 LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
3899 }
3900 }
3901
3902 #[cfg(feature = "artifact-graph")]
3903 fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
3904 match self {
3905 LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
3906 LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
3907 }
3908 }
3909}
3910
3911#[expect(unused)]
3913fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
3914 kcmc::shared::Angle::from_degrees(angle.to_degrees())
3915}
3916
3917#[expect(unused)]
3919fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
3920 ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
3921}
3922
3923pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3924 #[derive(Clone, Copy)]
3925 struct ConstrainableLine {
3926 solver_line: DatumLineSegment,
3927 #[cfg(feature = "artifact-graph")]
3928 object_id: ObjectId,
3929 }
3930
3931 let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3932 "lines",
3933 &RuntimeType::Array(
3934 Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3935 ArrayLen::Minimum(2),
3936 ),
3937 exec_state,
3938 )?;
3939 let range = args.source_range;
3940 let constrainable_lines: Vec<ConstrainableLine> = lines
3941 .iter()
3942 .map(|line| {
3943 let KclValue::Segment { value: segment } = line else {
3944 return Err(KclError::new_semantic(KclErrorDetails::new(
3945 "line argument must be a Segment".to_owned(),
3946 vec![args.source_range],
3947 )));
3948 };
3949 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3950 return Err(KclError::new_internal(KclErrorDetails::new(
3951 "line must be an unsolved Segment".to_owned(),
3952 vec![args.source_range],
3953 )));
3954 };
3955 let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3956 return Err(KclError::new_semantic(KclErrorDetails::new(
3957 "line argument must be a line, no other type of Segment".to_owned(),
3958 vec![args.source_range],
3959 )));
3960 };
3961 let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
3962 return Err(KclError::new_semantic(KclErrorDetails::new(
3963 "line's start x coordinate must be a var".to_owned(),
3964 vec![args.source_range],
3965 )));
3966 };
3967 let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
3968 return Err(KclError::new_semantic(KclErrorDetails::new(
3969 "line's start y coordinate must be a var".to_owned(),
3970 vec![args.source_range],
3971 )));
3972 };
3973 let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
3974 return Err(KclError::new_semantic(KclErrorDetails::new(
3975 "line's end x coordinate must be a var".to_owned(),
3976 vec![args.source_range],
3977 )));
3978 };
3979 let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
3980 return Err(KclError::new_semantic(KclErrorDetails::new(
3981 "line's end y coordinate must be a var".to_owned(),
3982 vec![args.source_range],
3983 )));
3984 };
3985
3986 let solver_line_p0 =
3987 DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
3988 let solver_line_p1 =
3989 DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
3990
3991 Ok(ConstrainableLine {
3992 solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
3993 #[cfg(feature = "artifact-graph")]
3994 object_id: unsolved.object_id,
3995 })
3996 })
3997 .collect::<Result<_, _>>()?;
3998
3999 #[cfg(feature = "artifact-graph")]
4000 let constraint_id = exec_state.next_object_id();
4001 let Some(sketch_state) = exec_state.sketch_block_mut() else {
4002 return Err(KclError::new_semantic(KclErrorDetails::new(
4003 "parallel() can only be used inside a sketch block".to_owned(),
4004 vec![args.source_range],
4005 )));
4006 };
4007
4008 let n = constrainable_lines.len();
4009 let mut constrainable_lines_iter = constrainable_lines.iter();
4010 let first_line = constrainable_lines_iter
4011 .next()
4012 .ok_or(KclError::new_semantic(KclErrorDetails::new(
4013 format!("parallel() requires at least 2 lines, but you provided {}", n),
4014 vec![args.source_range],
4015 )))?;
4016 for line in constrainable_lines_iter {
4017 sketch_state.solver_constraints.push(SolverConstraint::LinesAtAngle(
4018 first_line.solver_line,
4019 line.solver_line,
4020 AngleKind::Parallel,
4021 ));
4022 }
4023 #[cfg(feature = "artifact-graph")]
4024 {
4025 let constraint = Constraint::Parallel(Parallel {
4026 lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
4027 });
4028 sketch_state.sketch_constraints.push(constraint_id);
4029 track_constraint(constraint_id, constraint, exec_state, &args);
4030 }
4031 Ok(KclValue::none())
4032}
4033
4034pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4035 lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
4036}
4037
4038#[derive(Debug, Clone, Copy)]
4040enum AxisConstraintKind {
4041 Horizontal,
4042 Vertical,
4043}
4044
4045impl AxisConstraintKind {
4046 fn function_name(self) -> &'static str {
4048 match self {
4049 AxisConstraintKind::Horizontal => "horizontal",
4050 AxisConstraintKind::Vertical => "vertical",
4051 }
4052 }
4053
4054 fn line_constraint(self, line: DatumLineSegment) -> SolverConstraint {
4056 match self {
4057 AxisConstraintKind::Horizontal => SolverConstraint::Horizontal(line),
4058 AxisConstraintKind::Vertical => SolverConstraint::Vertical(line),
4059 }
4060 }
4061
4062 fn point_pair_constraint(self, p0: DatumPoint, p1: DatumPoint) -> SolverConstraint {
4064 match self {
4065 AxisConstraintKind::Horizontal => SolverConstraint::VerticalDistance(p1, p0, 0.0),
4067 AxisConstraintKind::Vertical => SolverConstraint::HorizontalDistance(p1, p0, 0.0),
4069 }
4070 }
4071
4072 fn constraint_aligning_point_to_constant(self, p0: DatumPoint, fixed_point: (f64, f64)) -> SolverConstraint {
4074 match self {
4075 AxisConstraintKind::Horizontal => SolverConstraint::Fixed(p0.y_id, fixed_point.1),
4076 AxisConstraintKind::Vertical => SolverConstraint::Fixed(p0.x_id, fixed_point.0),
4077 }
4078 }
4079
4080 #[cfg(feature = "artifact-graph")]
4081 fn line_artifact_constraint(self, line: ObjectId) -> Constraint {
4082 match self {
4083 AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Line { line }),
4084 AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Line { line }),
4085 }
4086 }
4087
4088 #[cfg(feature = "artifact-graph")]
4089 fn point_artifact_constraint(self, points: Vec<ConstraintSegment>) -> Constraint {
4090 match self {
4091 AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Points { points }),
4092 AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Points { points }),
4093 }
4094 }
4095}
4096
4097#[derive(Debug, Clone, Copy)]
4100struct AxisLineVars {
4101 start: [SketchVarId; 2],
4102 end: [SketchVarId; 2],
4103 #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
4104 object_id: ObjectId,
4105}
4106
4107fn extract_axis_line_vars(
4108 segment: &AbstractSegment,
4109 kind: AxisConstraintKind,
4110 source_range: crate::SourceRange,
4111) -> Result<AxisLineVars, KclError> {
4112 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4113 return Err(KclError::new_internal(KclErrorDetails::new(
4114 "line must be an unsolved Segment".to_owned(),
4115 vec![source_range],
4116 )));
4117 };
4118 let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
4119 return Err(KclError::new_semantic(KclErrorDetails::new(
4120 format!(
4121 "{}() line argument must be a line, no other type of Segment",
4122 kind.function_name()
4123 ),
4124 vec![source_range],
4125 )));
4126 };
4127 let (
4128 UnsolvedExpr::Unknown(start_x),
4129 UnsolvedExpr::Unknown(start_y),
4130 UnsolvedExpr::Unknown(end_x),
4131 UnsolvedExpr::Unknown(end_y),
4132 ) = (&start[0], &start[1], &end[0], &end[1])
4133 else {
4134 return Err(KclError::new_semantic(KclErrorDetails::new(
4135 "line's x and y coordinates of both start and end must be vars".to_owned(),
4136 vec![source_range],
4137 )));
4138 };
4139
4140 Ok(AxisLineVars {
4141 start: [*start_x, *start_y],
4142 end: [*end_x, *end_y],
4143 object_id: unsolved.object_id,
4144 })
4145}
4146
4147#[derive(Debug, Clone)]
4148enum PointToAlign {
4149 Variable { x: SketchVarId, y: SketchVarId },
4151 Fixed { x: TyF64, y: TyF64 },
4153}
4154
4155impl From<[SketchVarId; 2]> for PointToAlign {
4156 fn from(sketch_var: [SketchVarId; 2]) -> Self {
4157 Self::Variable {
4158 x: sketch_var[0],
4159 y: sketch_var[1],
4160 }
4161 }
4162}
4163
4164impl From<[TyF64; 2]> for PointToAlign {
4165 fn from([x, y]: [TyF64; 2]) -> Self {
4166 Self::Fixed { x, y }
4167 }
4168}
4169
4170fn extract_axis_point_vars(
4171 input: &KclValue,
4172 kind: AxisConstraintKind,
4173 source_range: crate::SourceRange,
4174) -> Result<PointToAlign, KclError> {
4175 match input {
4176 KclValue::Segment { value: segment } => {
4177 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4178 return Err(KclError::new_semantic(KclErrorDetails::new(
4179 format!(
4180 "The `{}` function point arguments must be unsolved points",
4181 kind.function_name()
4182 ),
4183 vec![source_range],
4184 )));
4185 };
4186 let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
4187 return Err(KclError::new_semantic(KclErrorDetails::new(
4188 format!(
4189 "The `{}` function list arguments must be points, but one item is {}",
4190 kind.function_name(),
4191 unsolved.kind.human_friendly_kind_with_article()
4192 ),
4193 vec![source_range],
4194 )));
4195 };
4196 match (&position[0], &position[1]) {
4197 (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed {
4198 x: x.to_owned(),
4199 y: y.to_owned(),
4200 }),
4201 (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x: *x, y: *y }),
4202 (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4203 Err(KclError::new_semantic(KclErrorDetails::new(
4204 format!(
4205 "The `{}` function cannot take a fixed X component and a variable Y component",
4206 kind.function_name()
4207 ),
4208 vec![source_range],
4209 )))
4210 }
4211 (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4212 Err(KclError::new_semantic(KclErrorDetails::new(
4213 format!(
4214 "The `{}` function cannot take a fixed X component and a variable Y component",
4215 kind.function_name()
4216 ),
4217 vec![source_range],
4218 )))
4219 }
4220 }
4221 }
4222 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4223 let [x_value, y_value] = value.as_slice() else {
4224 return Err(KclError::new_semantic(KclErrorDetails::new(
4225 format!(
4226 "The `{}` function point arguments must each be a Point2d like [var 0mm, var 0mm]",
4227 kind.function_name()
4228 ),
4229 vec![source_range],
4230 )));
4231 };
4232 let Some(x_expr) = x_value.as_unsolved_expr() else {
4233 return Err(KclError::new_semantic(KclErrorDetails::new(
4234 format!(
4235 "The `{}` function point x coordinate must be a number or sketch var",
4236 kind.function_name()
4237 ),
4238 vec![source_range],
4239 )));
4240 };
4241 let Some(y_expr) = y_value.as_unsolved_expr() else {
4242 return Err(KclError::new_semantic(KclErrorDetails::new(
4243 format!(
4244 "The `{}` function point y coordinate must be a number or sketch var",
4245 kind.function_name()
4246 ),
4247 vec![source_range],
4248 )));
4249 };
4250 match (x_expr, y_expr) {
4251 (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed { x, y }),
4252 (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x, y }),
4253 (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4254 Err(KclError::new_semantic(KclErrorDetails::new(
4255 format!(
4256 "The `{}` function cannot take a fixed X component and a variable Y component",
4257 kind.function_name()
4258 ),
4259 vec![source_range],
4260 )))
4261 }
4262 (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4263 Err(KclError::new_semantic(KclErrorDetails::new(
4264 format!(
4265 "The `{}` function cannot take a fixed X component and a variable Y component",
4266 kind.function_name()
4267 ),
4268 vec![source_range],
4269 )))
4270 }
4271 }
4272 }
4273 _ => Err(KclError::new_semantic(KclErrorDetails::new(
4274 format!(
4275 "The `{}` function accepts either a line Segment or a list of points",
4276 kind.function_name()
4277 ),
4278 vec![source_range],
4279 ))),
4280 }
4281}
4282
4283async fn axis_constraint(
4284 kind: AxisConstraintKind,
4285 exec_state: &mut ExecState,
4286 args: Args,
4287) -> Result<KclValue, KclError> {
4288 let input: KclValue =
4289 args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
4290
4291 match input {
4293 KclValue::Segment { value } => {
4294 axis_constraint_line(value, kind, exec_state, args)
4296 }
4297 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4298 axis_constraint_points(value, kind, exec_state, args)
4300 }
4301 other => Err(KclError::new_semantic(KclErrorDetails::new(
4302 format!(
4303 "{}() accepts either a line Segment or a list of at least two points, but you provided {}",
4304 kind.function_name(),
4305 other.human_friendly_type(),
4306 ),
4307 vec![args.source_range],
4308 ))),
4309 }
4310}
4311
4312fn axis_constraint_line(
4314 segment: Box<AbstractSegment>,
4315 kind: AxisConstraintKind,
4316 exec_state: &mut ExecState,
4317 args: Args,
4318) -> Result<KclValue, KclError> {
4319 let line = extract_axis_line_vars(&segment, kind, args.source_range)?;
4320 let range = args.source_range;
4321 let solver_p0 = DatumPoint::new_xy(
4322 line.start[0].to_constraint_id(range)?,
4323 line.start[1].to_constraint_id(range)?,
4324 );
4325 let solver_p1 = DatumPoint::new_xy(
4326 line.end[0].to_constraint_id(range)?,
4327 line.end[1].to_constraint_id(range)?,
4328 );
4329 let solver_line = DatumLineSegment::new(solver_p0, solver_p1);
4330 let constraint = kind.line_constraint(solver_line);
4331 #[cfg(feature = "artifact-graph")]
4332 let constraint_id = exec_state.next_object_id();
4333 let Some(sketch_state) = exec_state.sketch_block_mut() else {
4334 return Err(KclError::new_semantic(KclErrorDetails::new(
4335 format!("{}() can only be used inside a sketch block", kind.function_name()),
4336 vec![args.source_range],
4337 )));
4338 };
4339 sketch_state.solver_constraints.push(constraint);
4340 #[cfg(feature = "artifact-graph")]
4341 {
4342 let constraint = kind.line_artifact_constraint(line.object_id);
4343 sketch_state.sketch_constraints.push(constraint_id);
4344 track_constraint(constraint_id, constraint, exec_state, &args);
4345 }
4346 Ok(KclValue::none())
4347}
4348
4349fn axis_constraint_points(
4351 point_values: Vec<KclValue>,
4352 kind: AxisConstraintKind,
4353 exec_state: &mut ExecState,
4354 args: Args,
4355) -> Result<KclValue, KclError> {
4356 if point_values.len() < 2 {
4357 return Err(KclError::new_semantic(KclErrorDetails::new(
4358 format!("{}() point list must contain at least two points", kind.function_name()),
4359 vec![args.source_range],
4360 )));
4361 }
4362
4363 #[cfg(feature = "artifact-graph")]
4364 let trackable_point_ids = point_values
4365 .iter()
4366 .map(|point| match point {
4367 KclValue::Segment { value: segment } => {
4368 let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4369 return None;
4370 };
4371 let UnsolvedSegmentKind::Point { .. } = &unsolved.kind else {
4372 return None;
4373 };
4374 Some(ConstraintSegment::from(unsolved.object_id))
4375 }
4376 point if point2d_is_origin(point) => Some(ConstraintSegment::ORIGIN),
4377 _ => None,
4378 })
4379 .collect::<Option<Vec<_>>>();
4380
4381 let Some(sketch_state) = exec_state.sketch_block_mut() else {
4382 return Err(KclError::new_semantic(KclErrorDetails::new(
4383 format!("{}() can only be used inside a sketch block", kind.function_name()),
4384 vec![args.source_range],
4385 )));
4386 };
4387
4388 let points: Vec<PointToAlign> = point_values
4389 .iter()
4390 .map(|point| extract_axis_point_vars(point, kind, args.source_range))
4391 .collect::<Result<_, _>>()?;
4392
4393 let mut solver_constraints = Vec::with_capacity(points.len().saturating_sub(1));
4394
4395 let mut var_points = Vec::new();
4396 let mut fix_points = Vec::new();
4397 for point in points {
4398 match point {
4399 PointToAlign::Variable { x, y } => var_points.push((x, y)),
4400 PointToAlign::Fixed { x, y } => fix_points.push((x, y)),
4401 }
4402 }
4403 if fix_points.len() > 1 {
4404 return Err(KclError::new_semantic(KclErrorDetails::new(
4405 format!(
4406 "{}() point list can contain at most 1 fixed point, but you provided {}",
4407 kind.function_name(),
4408 fix_points.len()
4409 ),
4410 vec![args.source_range],
4411 )));
4412 }
4413
4414 if let Some(fix_point) = fix_points.pop() {
4415 for point in var_points {
4423 let solver_point = datum_point([point.0, point.1], args.source_range)?;
4424 let fix_point_mm = (fix_point.0.to_mm(), fix_point.1.to_mm());
4425 solver_constraints.push(kind.constraint_aligning_point_to_constant(solver_point, fix_point_mm));
4426 }
4427 } else {
4428 let mut points = var_points.into_iter();
4435 let first_point = points.next().ok_or_else(|| {
4436 KclError::new_semantic(KclErrorDetails::new(
4437 format!("{}() point list must contain at least two points", kind.function_name()),
4438 vec![args.source_range],
4439 ))
4440 })?;
4441 let anchor = datum_point([first_point.0, first_point.1], args.source_range)?;
4442 for point in points {
4443 let solver_point = datum_point([point.0, point.1], args.source_range)?;
4444 solver_constraints.push(kind.point_pair_constraint(anchor, solver_point));
4445 }
4446 }
4447 sketch_state.solver_constraints.extend(solver_constraints);
4448
4449 #[cfg(feature = "artifact-graph")]
4450 if let Some(point_ids) = trackable_point_ids {
4451 let constraint_id = exec_state.next_object_id();
4452 let Some(sketch_state) = exec_state.sketch_block_mut() else {
4453 debug_assert!(false, "Constraint created outside a sketch block");
4454 return Ok(KclValue::none());
4455 };
4456 sketch_state.sketch_constraints.push(constraint_id);
4457 let constraint = kind.point_artifact_constraint(point_ids);
4458 track_constraint(constraint_id, constraint, exec_state, &args);
4459 }
4460
4461 Ok(KclValue::none())
4462}
4463
4464pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4465 let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
4466 "lines",
4467 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
4468 exec_state,
4469 )?;
4470 let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
4471 KclError::new_semantic(KclErrorDetails::new(
4472 "must have two input lines".to_owned(),
4473 vec![args.source_range],
4474 ))
4475 })?;
4476 let KclValue::Segment { value: segment0 } = &line0 else {
4477 return Err(KclError::new_semantic(KclErrorDetails::new(
4478 "line argument must be a Segment".to_owned(),
4479 vec![args.source_range],
4480 )));
4481 };
4482 let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
4483 return Err(KclError::new_internal(KclErrorDetails::new(
4484 "line must be an unsolved Segment".to_owned(),
4485 vec![args.source_range],
4486 )));
4487 };
4488 let UnsolvedSegmentKind::Line {
4489 start: start0,
4490 end: end0,
4491 ..
4492 } = &unsolved0.kind
4493 else {
4494 return Err(KclError::new_semantic(KclErrorDetails::new(
4495 "line argument must be a line, no other type of Segment".to_owned(),
4496 vec![args.source_range],
4497 )));
4498 };
4499 let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
4500 return Err(KclError::new_semantic(KclErrorDetails::new(
4501 "line's start x coordinate must be a var".to_owned(),
4502 vec![args.source_range],
4503 )));
4504 };
4505 let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
4506 return Err(KclError::new_semantic(KclErrorDetails::new(
4507 "line's start y coordinate must be a var".to_owned(),
4508 vec![args.source_range],
4509 )));
4510 };
4511 let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
4512 return Err(KclError::new_semantic(KclErrorDetails::new(
4513 "line's end x coordinate must be a var".to_owned(),
4514 vec![args.source_range],
4515 )));
4516 };
4517 let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
4518 return Err(KclError::new_semantic(KclErrorDetails::new(
4519 "line's end y coordinate must be a var".to_owned(),
4520 vec![args.source_range],
4521 )));
4522 };
4523 let KclValue::Segment { value: segment1 } = &line1 else {
4524 return Err(KclError::new_semantic(KclErrorDetails::new(
4525 "line argument must be a Segment".to_owned(),
4526 vec![args.source_range],
4527 )));
4528 };
4529 let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
4530 return Err(KclError::new_internal(KclErrorDetails::new(
4531 "line must be an unsolved Segment".to_owned(),
4532 vec![args.source_range],
4533 )));
4534 };
4535 let UnsolvedSegmentKind::Line {
4536 start: start1,
4537 end: end1,
4538 ..
4539 } = &unsolved1.kind
4540 else {
4541 return Err(KclError::new_semantic(KclErrorDetails::new(
4542 "line argument must be a line, no other type of Segment".to_owned(),
4543 vec![args.source_range],
4544 )));
4545 };
4546 let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
4547 return Err(KclError::new_semantic(KclErrorDetails::new(
4548 "line's start x coordinate must be a var".to_owned(),
4549 vec![args.source_range],
4550 )));
4551 };
4552 let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
4553 return Err(KclError::new_semantic(KclErrorDetails::new(
4554 "line's start y coordinate must be a var".to_owned(),
4555 vec![args.source_range],
4556 )));
4557 };
4558 let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
4559 return Err(KclError::new_semantic(KclErrorDetails::new(
4560 "line's end x coordinate must be a var".to_owned(),
4561 vec![args.source_range],
4562 )));
4563 };
4564 let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
4565 return Err(KclError::new_semantic(KclErrorDetails::new(
4566 "line's end y coordinate must be a var".to_owned(),
4567 vec![args.source_range],
4568 )));
4569 };
4570
4571 let sketch_constraint = SketchConstraint {
4573 kind: SketchConstraintKind::Angle {
4574 line0: crate::execution::ConstrainableLine2d {
4575 object_id: unsolved0.object_id,
4576 vars: [
4577 crate::front::Point2d {
4578 x: *line0_p0_x,
4579 y: *line0_p0_y,
4580 },
4581 crate::front::Point2d {
4582 x: *line0_p1_x,
4583 y: *line0_p1_y,
4584 },
4585 ],
4586 },
4587 line1: crate::execution::ConstrainableLine2d {
4588 object_id: unsolved1.object_id,
4589 vars: [
4590 crate::front::Point2d {
4591 x: *line1_p0_x,
4592 y: *line1_p0_y,
4593 },
4594 crate::front::Point2d {
4595 x: *line1_p1_x,
4596 y: *line1_p1_y,
4597 },
4598 ],
4599 },
4600 },
4601 meta: vec![args.source_range.into()],
4602 };
4603 Ok(KclValue::SketchConstraint {
4604 value: Box::new(sketch_constraint),
4605 })
4606}
4607
4608async fn lines_at_angle(
4609 angle_kind: LinesAtAngleKind,
4610 exec_state: &mut ExecState,
4611 args: Args,
4612) -> Result<KclValue, KclError> {
4613 let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
4614 "lines",
4615 &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
4616 exec_state,
4617 )?;
4618 let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
4619 KclError::new_semantic(KclErrorDetails::new(
4620 "must have two input lines".to_owned(),
4621 vec![args.source_range],
4622 ))
4623 })?;
4624
4625 let KclValue::Segment { value: segment0 } = &line0 else {
4626 return Err(KclError::new_semantic(KclErrorDetails::new(
4627 "line argument must be a Segment".to_owned(),
4628 vec![args.source_range],
4629 )));
4630 };
4631 let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
4632 return Err(KclError::new_internal(KclErrorDetails::new(
4633 "line must be an unsolved Segment".to_owned(),
4634 vec![args.source_range],
4635 )));
4636 };
4637 let UnsolvedSegmentKind::Line {
4638 start: start0,
4639 end: end0,
4640 ..
4641 } = &unsolved0.kind
4642 else {
4643 return Err(KclError::new_semantic(KclErrorDetails::new(
4644 "line argument must be a line, no other type of Segment".to_owned(),
4645 vec![args.source_range],
4646 )));
4647 };
4648 let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
4649 return Err(KclError::new_semantic(KclErrorDetails::new(
4650 "line's start x coordinate must be a var".to_owned(),
4651 vec![args.source_range],
4652 )));
4653 };
4654 let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
4655 return Err(KclError::new_semantic(KclErrorDetails::new(
4656 "line's start y coordinate must be a var".to_owned(),
4657 vec![args.source_range],
4658 )));
4659 };
4660 let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
4661 return Err(KclError::new_semantic(KclErrorDetails::new(
4662 "line's end x coordinate must be a var".to_owned(),
4663 vec![args.source_range],
4664 )));
4665 };
4666 let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
4667 return Err(KclError::new_semantic(KclErrorDetails::new(
4668 "line's end y coordinate must be a var".to_owned(),
4669 vec![args.source_range],
4670 )));
4671 };
4672 let KclValue::Segment { value: segment1 } = &line1 else {
4673 return Err(KclError::new_semantic(KclErrorDetails::new(
4674 "line argument must be a Segment".to_owned(),
4675 vec![args.source_range],
4676 )));
4677 };
4678 let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
4679 return Err(KclError::new_internal(KclErrorDetails::new(
4680 "line must be an unsolved Segment".to_owned(),
4681 vec![args.source_range],
4682 )));
4683 };
4684 let UnsolvedSegmentKind::Line {
4685 start: start1,
4686 end: end1,
4687 ..
4688 } = &unsolved1.kind
4689 else {
4690 return Err(KclError::new_semantic(KclErrorDetails::new(
4691 "line argument must be a line, no other type of Segment".to_owned(),
4692 vec![args.source_range],
4693 )));
4694 };
4695 let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
4696 return Err(KclError::new_semantic(KclErrorDetails::new(
4697 "line's start x coordinate must be a var".to_owned(),
4698 vec![args.source_range],
4699 )));
4700 };
4701 let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
4702 return Err(KclError::new_semantic(KclErrorDetails::new(
4703 "line's start y coordinate must be a var".to_owned(),
4704 vec![args.source_range],
4705 )));
4706 };
4707 let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
4708 return Err(KclError::new_semantic(KclErrorDetails::new(
4709 "line's end x coordinate must be a var".to_owned(),
4710 vec![args.source_range],
4711 )));
4712 };
4713 let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
4714 return Err(KclError::new_semantic(KclErrorDetails::new(
4715 "line's end y coordinate must be a var".to_owned(),
4716 vec![args.source_range],
4717 )));
4718 };
4719
4720 let range = args.source_range;
4721 let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4722 line0_p0_x.to_constraint_id(range)?,
4723 line0_p0_y.to_constraint_id(range)?,
4724 );
4725 let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4726 line0_p1_x.to_constraint_id(range)?,
4727 line0_p1_y.to_constraint_id(range)?,
4728 );
4729 let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
4730 let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4731 line1_p0_x.to_constraint_id(range)?,
4732 line1_p0_y.to_constraint_id(range)?,
4733 );
4734 let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4735 line1_p1_x.to_constraint_id(range)?,
4736 line1_p1_y.to_constraint_id(range)?,
4737 );
4738 let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
4739 let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
4740 #[cfg(feature = "artifact-graph")]
4741 let constraint_id = exec_state.next_object_id();
4742 let Some(sketch_state) = exec_state.sketch_block_mut() else {
4744 return Err(KclError::new_semantic(KclErrorDetails::new(
4745 format!(
4746 "{}() can only be used inside a sketch block",
4747 angle_kind.to_function_name()
4748 ),
4749 vec![args.source_range],
4750 )));
4751 };
4752 sketch_state.solver_constraints.push(constraint);
4753 #[cfg(feature = "artifact-graph")]
4754 {
4755 let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
4756 sketch_state.sketch_constraints.push(constraint_id);
4757 track_constraint(constraint_id, constraint, exec_state, &args);
4758 }
4759 Ok(KclValue::none())
4760}
4761
4762pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4763 axis_constraint(AxisConstraintKind::Horizontal, exec_state, args).await
4764}
4765
4766pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4767 axis_constraint(AxisConstraintKind::Vertical, exec_state, args).await
4768}