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