Skip to main content

kcl_lib/std/
constraints.rs

1use anyhow::Result;
2use ezpz::Constraint as SolverConstraint;
3use ezpz::datatypes::AngleKind;
4use ezpz::datatypes::inputs::DatumCircle;
5use ezpz::datatypes::inputs::DatumCircularArc;
6use ezpz::datatypes::inputs::DatumDistance;
7use ezpz::datatypes::inputs::DatumLineSegment;
8use ezpz::datatypes::inputs::DatumPoint;
9use kittycad_modeling_cmds as kcmc;
10
11use crate::errors::KclError;
12use crate::errors::KclErrorDetails;
13use crate::execution::AbstractSegment;
14#[cfg(feature = "artifact-graph")]
15use crate::execution::Artifact;
16#[cfg(feature = "artifact-graph")]
17use crate::execution::CodeRef;
18use crate::execution::ConstrainablePoint2d;
19use crate::execution::ConstrainablePoint2dOrOrigin;
20use crate::execution::ExecState;
21use crate::execution::KclValue;
22use crate::execution::SegmentRepr;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::SketchBlockConstraint;
25#[cfg(feature = "artifact-graph")]
26use crate::execution::SketchBlockConstraintType;
27use crate::execution::SketchConstraint;
28use crate::execution::SketchConstraintKind;
29use crate::execution::SketchVarId;
30use crate::execution::UnsolvedExpr;
31use crate::execution::UnsolvedSegment;
32use crate::execution::UnsolvedSegmentKind;
33use crate::execution::normalize_to_solver_distance_unit;
34use crate::execution::solver_numeric_type;
35use crate::execution::types::ArrayLen;
36use crate::execution::types::PrimitiveType;
37use crate::execution::types::RuntimeType;
38use crate::front::ArcCtor;
39use crate::front::CircleCtor;
40#[cfg(feature = "artifact-graph")]
41use crate::front::Coincident;
42#[cfg(feature = "artifact-graph")]
43use crate::front::Constraint;
44#[cfg(feature = "artifact-graph")]
45use crate::front::EqualRadius;
46#[cfg(feature = "artifact-graph")]
47use crate::front::Horizontal;
48use crate::front::LineCtor;
49#[cfg(feature = "artifact-graph")]
50use crate::front::LinesEqualLength;
51#[cfg(feature = "artifact-graph")]
52use crate::front::Object;
53use crate::front::ObjectId;
54#[cfg(feature = "artifact-graph")]
55use crate::front::ObjectKind;
56#[cfg(feature = "artifact-graph")]
57use crate::front::Parallel;
58#[cfg(feature = "artifact-graph")]
59use crate::front::Perpendicular;
60use crate::front::Point2d;
61use crate::front::PointCtor;
62#[cfg(feature = "artifact-graph")]
63use crate::front::SourceRef;
64#[cfg(feature = "artifact-graph")]
65use crate::front::Tangent;
66#[cfg(feature = "artifact-graph")]
67use crate::front::Vertical;
68#[cfg(feature = "artifact-graph")]
69use crate::frontend::sketch::ConstraintSegment;
70use crate::std::Args;
71use crate::std::args::FromKclValue;
72use crate::std::args::TyF64;
73
74fn point2d_is_origin(point2d: &KclValue) -> bool {
75    let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point2d) else {
76        return false;
77    };
78    // Both components must be lengths (not angles or unknown types).
79    // as_length() returns None for non-length types.
80    if x.ty.as_length().is_none() || y.ty.as_length().is_none() {
81        return false;
82    }
83    // Now that we've checked that they're lengths, the exact units don't
84    // matter. We only care that the value is zero.
85    x.n == 0.0 && y.n == 0.0
86}
87
88/// Arcs have 6 scalar values (start, end and center; x and y).
89/// These could be fixed constants or sketch variables to be solved.
90/// Each of these needs a sketch variable to feed into the solver.
91/// If it's a solver variable, then use it.
92/// If it's a fixed constant, then create a solver variable for it,
93/// and return a constraint to fix it.
94fn extract_arc_component(
95    value: &KclValue,
96    exec_state: &mut ExecState,
97    range: crate::SourceRange,
98    description: &str,
99) -> Result<(SketchVarId, Option<SolverConstraint>), KclError> {
100    match value.as_unsolved_expr() {
101        None => Err(KclError::new_semantic(KclErrorDetails::new(
102            format!("{description} must be a number or sketch var"),
103            vec![range],
104        ))),
105        Some(UnsolvedExpr::Unknown(var_id)) => Ok((var_id, None)),
106        Some(UnsolvedExpr::Known(_)) => {
107            let value_in_solver_units = normalize_to_solver_distance_unit(value, range, exec_state, description)?;
108            let Some(normalized_value) = value_in_solver_units.as_ty_f64() else {
109                return Err(KclError::new_internal(KclErrorDetails::new(
110                    "Expected number after coercion".to_owned(),
111                    vec![range],
112                )));
113            };
114
115            let Some(sketch_state) = exec_state.sketch_block_mut() else {
116                return Err(KclError::new_semantic(KclErrorDetails::new(
117                    "arc() can only be used inside a sketch block".to_owned(),
118                    vec![range],
119                )));
120            };
121            let var_id = sketch_state.next_sketch_var_id();
122            sketch_state.sketch_vars.push(KclValue::SketchVar {
123                value: Box::new(crate::execution::SketchVar {
124                    id: var_id,
125                    initial_value: normalized_value.n,
126                    ty: normalized_value.ty,
127                    meta: vec![],
128                }),
129            });
130
131            Ok((
132                var_id,
133                Some(SolverConstraint::Fixed(
134                    var_id.to_constraint_id(range)?,
135                    normalized_value.n,
136                )),
137            ))
138        }
139    }
140}
141
142#[cfg(feature = "artifact-graph")]
143fn coincident_segments_for_segment_and_point2d(
144    segment_id: ObjectId,
145    point2d: &KclValue,
146    segment_first: bool,
147) -> Vec<ConstraintSegment> {
148    if !point2d_is_origin(point2d) {
149        return vec![segment_id.into()];
150    }
151
152    if segment_first {
153        vec![segment_id.into(), ConstraintSegment::ORIGIN]
154    } else {
155        vec![ConstraintSegment::ORIGIN, segment_id.into()]
156    }
157}
158
159pub async fn point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
160    let at: Vec<KclValue> = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
161    let [at_x_value, at_y_value]: [KclValue; 2] = at.try_into().map_err(|_| {
162        KclError::new_semantic(KclErrorDetails::new(
163            "at must be a 2D point".to_owned(),
164            vec![args.source_range],
165        ))
166    })?;
167    let Some(at_x) = at_x_value.as_unsolved_expr() else {
168        return Err(KclError::new_semantic(KclErrorDetails::new(
169            "at x must be a number or sketch var".to_owned(),
170            vec![args.source_range],
171        )));
172    };
173    let Some(at_y) = at_y_value.as_unsolved_expr() else {
174        return Err(KclError::new_semantic(KclErrorDetails::new(
175            "at y must be a number or sketch var".to_owned(),
176            vec![args.source_range],
177        )));
178    };
179    let ctor = PointCtor {
180        position: Point2d {
181            x: at_x_value.to_sketch_expr().ok_or_else(|| {
182                KclError::new_semantic(KclErrorDetails::new(
183                    "unable to convert numeric type to suffix".to_owned(),
184                    vec![args.source_range],
185                ))
186            })?,
187            y: at_y_value.to_sketch_expr().ok_or_else(|| {
188                KclError::new_semantic(KclErrorDetails::new(
189                    "unable to convert numeric type to suffix".to_owned(),
190                    vec![args.source_range],
191                ))
192            })?,
193        },
194    };
195    let segment = UnsolvedSegment {
196        id: exec_state.next_uuid(),
197        object_id: exec_state.next_object_id(),
198        kind: UnsolvedSegmentKind::Point {
199            position: [at_x, at_y],
200            ctor: Box::new(ctor),
201        },
202        tag: None,
203        node_path: args.node_path.clone(),
204        meta: vec![args.source_range.into()],
205    };
206    #[cfg(feature = "artifact-graph")]
207    let optional_constraints = {
208        let object_id = exec_state.add_placeholder_scene_object(segment.object_id, args.source_range, args.node_path);
209
210        let mut optional_constraints = Vec::new();
211        if exec_state.segment_ids_edited_contains(&object_id) {
212            if let Some(at_x_var) = at_x_value.as_sketch_var() {
213                let x_initial_value = at_x_var.initial_value_to_solver_units(
214                    exec_state,
215                    args.source_range,
216                    "edited segment fixed constraint value",
217                )?;
218                optional_constraints.push(SolverConstraint::Fixed(
219                    at_x_var.id.to_constraint_id(args.source_range)?,
220                    x_initial_value.n,
221                ));
222            }
223            if let Some(at_y_var) = at_y_value.as_sketch_var() {
224                let y_initial_value = at_y_var.initial_value_to_solver_units(
225                    exec_state,
226                    args.source_range,
227                    "edited segment fixed constraint value",
228                )?;
229                optional_constraints.push(SolverConstraint::Fixed(
230                    at_y_var.id.to_constraint_id(args.source_range)?,
231                    y_initial_value.n,
232                ));
233            }
234        }
235        optional_constraints
236    };
237
238    // Save the segment to be sent to the engine after solving.
239    let Some(sketch_state) = exec_state.sketch_block_mut() else {
240        return Err(KclError::new_semantic(KclErrorDetails::new(
241            "line() can only be used inside a sketch block".to_owned(),
242            vec![args.source_range],
243        )));
244    };
245    sketch_state.needed_by_engine.push(segment.clone());
246
247    #[cfg(feature = "artifact-graph")]
248    sketch_state.solver_optional_constraints.extend(optional_constraints);
249
250    let meta = segment.meta.clone();
251    let abstract_segment = AbstractSegment {
252        repr: SegmentRepr::Unsolved {
253            segment: Box::new(segment),
254        },
255        meta,
256    };
257    Ok(KclValue::Segment {
258        value: Box::new(abstract_segment),
259    })
260}
261
262pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
263    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
264    let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
265    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
266    let construction: bool = construction_opt.unwrap_or(false);
267    let construction_ctor = construction_opt;
268    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
269        KclError::new_semantic(KclErrorDetails::new(
270            "start must be a 2D point".to_owned(),
271            vec![args.source_range],
272        ))
273    })?;
274    let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
275        KclError::new_semantic(KclErrorDetails::new(
276            "end must be a 2D point".to_owned(),
277            vec![args.source_range],
278        ))
279    })?;
280    let Some(start_x) = start_x_value.as_unsolved_expr() else {
281        return Err(KclError::new_semantic(KclErrorDetails::new(
282            "start x must be a number or sketch var".to_owned(),
283            vec![args.source_range],
284        )));
285    };
286    let Some(start_y) = start_y_value.as_unsolved_expr() else {
287        return Err(KclError::new_semantic(KclErrorDetails::new(
288            "start y must be a number or sketch var".to_owned(),
289            vec![args.source_range],
290        )));
291    };
292    let Some(end_x) = end_x_value.as_unsolved_expr() else {
293        return Err(KclError::new_semantic(KclErrorDetails::new(
294            "end x must be a number or sketch var".to_owned(),
295            vec![args.source_range],
296        )));
297    };
298    let Some(end_y) = end_y_value.as_unsolved_expr() else {
299        return Err(KclError::new_semantic(KclErrorDetails::new(
300            "end y must be a number or sketch var".to_owned(),
301            vec![args.source_range],
302        )));
303    };
304    let ctor = LineCtor {
305        start: Point2d {
306            x: start_x_value.to_sketch_expr().ok_or_else(|| {
307                KclError::new_semantic(KclErrorDetails::new(
308                    "unable to convert numeric type to suffix".to_owned(),
309                    vec![args.source_range],
310                ))
311            })?,
312            y: start_y_value.to_sketch_expr().ok_or_else(|| {
313                KclError::new_semantic(KclErrorDetails::new(
314                    "unable to convert numeric type to suffix".to_owned(),
315                    vec![args.source_range],
316                ))
317            })?,
318        },
319        end: Point2d {
320            x: end_x_value.to_sketch_expr().ok_or_else(|| {
321                KclError::new_semantic(KclErrorDetails::new(
322                    "unable to convert numeric type to suffix".to_owned(),
323                    vec![args.source_range],
324                ))
325            })?,
326            y: end_y_value.to_sketch_expr().ok_or_else(|| {
327                KclError::new_semantic(KclErrorDetails::new(
328                    "unable to convert numeric type to suffix".to_owned(),
329                    vec![args.source_range],
330                ))
331            })?,
332        },
333        construction: construction_ctor,
334    };
335    // Order of ID generation is important.
336    let start_object_id = exec_state.next_object_id();
337    let end_object_id = exec_state.next_object_id();
338    let line_object_id = exec_state.next_object_id();
339    let segment = UnsolvedSegment {
340        id: exec_state.next_uuid(),
341        object_id: line_object_id,
342        kind: UnsolvedSegmentKind::Line {
343            start: [start_x, start_y],
344            end: [end_x, end_y],
345            ctor: Box::new(ctor),
346            start_object_id,
347            end_object_id,
348            construction,
349        },
350        tag: None,
351        node_path: args.node_path.clone(),
352        meta: vec![args.source_range.into()],
353    };
354    #[cfg(feature = "artifact-graph")]
355    let optional_constraints = {
356        let start_object_id =
357            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
358        let end_object_id =
359            exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
360        let line_object_id =
361            exec_state.add_placeholder_scene_object(line_object_id, args.source_range, args.node_path.clone());
362
363        let mut optional_constraints = Vec::new();
364        if exec_state.segment_ids_edited_contains(&start_object_id)
365            || exec_state.segment_ids_edited_contains(&line_object_id)
366        {
367            if let Some(start_x_var) = start_x_value.as_sketch_var() {
368                let x_initial_value = start_x_var.initial_value_to_solver_units(
369                    exec_state,
370                    args.source_range,
371                    "edited segment fixed constraint value",
372                )?;
373                optional_constraints.push(SolverConstraint::Fixed(
374                    start_x_var.id.to_constraint_id(args.source_range)?,
375                    x_initial_value.n,
376                ));
377            }
378            if let Some(start_y_var) = start_y_value.as_sketch_var() {
379                let y_initial_value = start_y_var.initial_value_to_solver_units(
380                    exec_state,
381                    args.source_range,
382                    "edited segment fixed constraint value",
383                )?;
384                optional_constraints.push(SolverConstraint::Fixed(
385                    start_y_var.id.to_constraint_id(args.source_range)?,
386                    y_initial_value.n,
387                ));
388            }
389        }
390        if exec_state.segment_ids_edited_contains(&end_object_id)
391            || exec_state.segment_ids_edited_contains(&line_object_id)
392        {
393            if let Some(end_x_var) = end_x_value.as_sketch_var() {
394                let x_initial_value = end_x_var.initial_value_to_solver_units(
395                    exec_state,
396                    args.source_range,
397                    "edited segment fixed constraint value",
398                )?;
399                optional_constraints.push(SolverConstraint::Fixed(
400                    end_x_var.id.to_constraint_id(args.source_range)?,
401                    x_initial_value.n,
402                ));
403            }
404            if let Some(end_y_var) = end_y_value.as_sketch_var() {
405                let y_initial_value = end_y_var.initial_value_to_solver_units(
406                    exec_state,
407                    args.source_range,
408                    "edited segment fixed constraint value",
409                )?;
410                optional_constraints.push(SolverConstraint::Fixed(
411                    end_y_var.id.to_constraint_id(args.source_range)?,
412                    y_initial_value.n,
413                ));
414            }
415        }
416        optional_constraints
417    };
418
419    // Save the segment to be sent to the engine after solving.
420    let Some(sketch_state) = exec_state.sketch_block_mut() else {
421        return Err(KclError::new_semantic(KclErrorDetails::new(
422            "line() can only be used inside a sketch block".to_owned(),
423            vec![args.source_range],
424        )));
425    };
426    sketch_state.needed_by_engine.push(segment.clone());
427
428    #[cfg(feature = "artifact-graph")]
429    sketch_state.solver_optional_constraints.extend(optional_constraints);
430
431    let meta = segment.meta.clone();
432    let abstract_segment = AbstractSegment {
433        repr: SegmentRepr::Unsolved {
434            segment: Box::new(segment),
435        },
436        meta,
437    };
438    Ok(KclValue::Segment {
439        value: Box::new(abstract_segment),
440    })
441}
442
443pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
444    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
445    let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
446    // TODO: make this optional and add interior.
447    let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
448    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
449    let construction: bool = construction_opt.unwrap_or(false);
450    let construction_ctor = construction_opt;
451
452    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
453        KclError::new_semantic(KclErrorDetails::new(
454            "start must be a 2D point".to_owned(),
455            vec![args.source_range],
456        ))
457    })?;
458    let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
459        KclError::new_semantic(KclErrorDetails::new(
460            "end must be a 2D point".to_owned(),
461            vec![args.source_range],
462        ))
463    })?;
464    let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
465        KclError::new_semantic(KclErrorDetails::new(
466            "center must be a 2D point".to_owned(),
467            vec![args.source_range],
468        ))
469    })?;
470
471    let (start_x, start_x_fixed) = extract_arc_component(&start_x_value, exec_state, args.source_range, "start x")?;
472    let (start_y, start_y_fixed) = extract_arc_component(&start_y_value, exec_state, args.source_range, "start y")?;
473    let (end_x, end_x_fixed) = extract_arc_component(&end_x_value, exec_state, args.source_range, "end x")?;
474    let (end_y, end_y_fixed) = extract_arc_component(&end_y_value, exec_state, args.source_range, "end y")?;
475    let (center_x, center_x_fixed) = extract_arc_component(&center_x_value, exec_state, args.source_range, "center x")?;
476    let (center_y, center_y_fixed) = extract_arc_component(&center_y_value, exec_state, args.source_range, "center y")?;
477    // If any of the points had any components that were fixed, then they'll become constraints
478    // in this list.
479    let arc_fixed_constraints = [
480        start_x_fixed,
481        start_y_fixed,
482        end_x_fixed,
483        end_y_fixed,
484        center_x_fixed,
485        center_y_fixed,
486    ]
487    .into_iter()
488    .flatten();
489
490    let ctor = ArcCtor {
491        start: Point2d {
492            x: start_x_value.to_sketch_expr().ok_or_else(|| {
493                KclError::new_semantic(KclErrorDetails::new(
494                    "unable to convert numeric type to suffix".to_owned(),
495                    vec![args.source_range],
496                ))
497            })?,
498            y: start_y_value.to_sketch_expr().ok_or_else(|| {
499                KclError::new_semantic(KclErrorDetails::new(
500                    "unable to convert numeric type to suffix".to_owned(),
501                    vec![args.source_range],
502                ))
503            })?,
504        },
505        end: Point2d {
506            x: end_x_value.to_sketch_expr().ok_or_else(|| {
507                KclError::new_semantic(KclErrorDetails::new(
508                    "unable to convert numeric type to suffix".to_owned(),
509                    vec![args.source_range],
510                ))
511            })?,
512            y: end_y_value.to_sketch_expr().ok_or_else(|| {
513                KclError::new_semantic(KclErrorDetails::new(
514                    "unable to convert numeric type to suffix".to_owned(),
515                    vec![args.source_range],
516                ))
517            })?,
518        },
519        center: Point2d {
520            x: center_x_value.to_sketch_expr().ok_or_else(|| {
521                KclError::new_semantic(KclErrorDetails::new(
522                    "unable to convert numeric type to suffix".to_owned(),
523                    vec![args.source_range],
524                ))
525            })?,
526            y: center_y_value.to_sketch_expr().ok_or_else(|| {
527                KclError::new_semantic(KclErrorDetails::new(
528                    "unable to convert numeric type to suffix".to_owned(),
529                    vec![args.source_range],
530                ))
531            })?,
532        },
533        construction: construction_ctor,
534    };
535
536    // Order of ID generation is important.
537    let start_object_id = exec_state.next_object_id();
538    let end_object_id = exec_state.next_object_id();
539    let center_object_id = exec_state.next_object_id();
540    let arc_object_id = exec_state.next_object_id();
541    let segment = UnsolvedSegment {
542        id: exec_state.next_uuid(),
543        object_id: arc_object_id,
544        kind: UnsolvedSegmentKind::Arc {
545            start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
546            end: [UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)],
547            center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
548            ctor: Box::new(ctor),
549            start_object_id,
550            end_object_id,
551            center_object_id,
552            construction,
553        },
554        tag: None,
555        node_path: args.node_path.clone(),
556        meta: vec![args.source_range.into()],
557    };
558    #[cfg(feature = "artifact-graph")]
559    let optional_constraints = {
560        let start_object_id =
561            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
562        let end_object_id =
563            exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
564        let center_object_id =
565            exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
566        let arc_object_id =
567            exec_state.add_placeholder_scene_object(arc_object_id, args.source_range, args.node_path.clone());
568
569        let mut optional_constraints = Vec::new();
570        if exec_state.segment_ids_edited_contains(&start_object_id)
571            || exec_state.segment_ids_edited_contains(&arc_object_id)
572        {
573            if let Some(start_x_var) = start_x_value.as_sketch_var() {
574                let x_initial_value = start_x_var.initial_value_to_solver_units(
575                    exec_state,
576                    args.source_range,
577                    "edited segment fixed constraint value",
578                )?;
579                optional_constraints.push(ezpz::Constraint::Fixed(
580                    start_x_var.id.to_constraint_id(args.source_range)?,
581                    x_initial_value.n,
582                ));
583            }
584            if let Some(start_y_var) = start_y_value.as_sketch_var() {
585                let y_initial_value = start_y_var.initial_value_to_solver_units(
586                    exec_state,
587                    args.source_range,
588                    "edited segment fixed constraint value",
589                )?;
590                optional_constraints.push(ezpz::Constraint::Fixed(
591                    start_y_var.id.to_constraint_id(args.source_range)?,
592                    y_initial_value.n,
593                ));
594            }
595        }
596        if exec_state.segment_ids_edited_contains(&end_object_id)
597            || exec_state.segment_ids_edited_contains(&arc_object_id)
598        {
599            if let Some(end_x_var) = end_x_value.as_sketch_var() {
600                let x_initial_value = end_x_var.initial_value_to_solver_units(
601                    exec_state,
602                    args.source_range,
603                    "edited segment fixed constraint value",
604                )?;
605                optional_constraints.push(ezpz::Constraint::Fixed(
606                    end_x_var.id.to_constraint_id(args.source_range)?,
607                    x_initial_value.n,
608                ));
609            }
610            if let Some(end_y_var) = end_y_value.as_sketch_var() {
611                let y_initial_value = end_y_var.initial_value_to_solver_units(
612                    exec_state,
613                    args.source_range,
614                    "edited segment fixed constraint value",
615                )?;
616                optional_constraints.push(ezpz::Constraint::Fixed(
617                    end_y_var.id.to_constraint_id(args.source_range)?,
618                    y_initial_value.n,
619                ));
620            }
621        }
622        if exec_state.segment_ids_edited_contains(&center_object_id)
623            || exec_state.segment_ids_edited_contains(&arc_object_id)
624        {
625            if let Some(center_x_var) = center_x_value.as_sketch_var() {
626                let x_initial_value = center_x_var.initial_value_to_solver_units(
627                    exec_state,
628                    args.source_range,
629                    "edited segment fixed constraint value",
630                )?;
631                optional_constraints.push(ezpz::Constraint::Fixed(
632                    center_x_var.id.to_constraint_id(args.source_range)?,
633                    x_initial_value.n,
634                ));
635            }
636            if let Some(center_y_var) = center_y_value.as_sketch_var() {
637                let y_initial_value = center_y_var.initial_value_to_solver_units(
638                    exec_state,
639                    args.source_range,
640                    "edited segment fixed constraint value",
641                )?;
642                optional_constraints.push(ezpz::Constraint::Fixed(
643                    center_y_var.id.to_constraint_id(args.source_range)?,
644                    y_initial_value.n,
645                ));
646            }
647        }
648        optional_constraints
649    };
650
651    // Build the implicit arc constraint.
652    let range = args.source_range;
653    let mut required_constraints = Vec::with_capacity(7);
654    required_constraints.extend(arc_fixed_constraints);
655    required_constraints.push(ezpz::Constraint::Arc(ezpz::datatypes::inputs::DatumCircularArc {
656        center: ezpz::datatypes::inputs::DatumPoint::new_xy(
657            center_x.to_constraint_id(range)?,
658            center_y.to_constraint_id(range)?,
659        ),
660        start: ezpz::datatypes::inputs::DatumPoint::new_xy(
661            start_x.to_constraint_id(range)?,
662            start_y.to_constraint_id(range)?,
663        ),
664        end: ezpz::datatypes::inputs::DatumPoint::new_xy(
665            end_x.to_constraint_id(range)?,
666            end_y.to_constraint_id(range)?,
667        ),
668    }));
669
670    let Some(sketch_state) = exec_state.sketch_block_mut() else {
671        return Err(KclError::new_semantic(KclErrorDetails::new(
672            "arc() can only be used inside a sketch block".to_owned(),
673            vec![args.source_range],
674        )));
675    };
676    // Save the segment to be sent to the engine after solving.
677    sketch_state.needed_by_engine.push(segment.clone());
678    // Save the constraints to be used for solving.
679    sketch_state.solver_constraints.extend(required_constraints);
680    // The constraint isn't added to scene objects since it's implicit in the
681    // arc segment. You cannot have an arc without it.
682
683    #[cfg(feature = "artifact-graph")]
684    sketch_state.solver_optional_constraints.extend(optional_constraints);
685
686    let meta = segment.meta.clone();
687    let abstract_segment = AbstractSegment {
688        repr: SegmentRepr::Unsolved {
689            segment: Box::new(segment),
690        },
691        meta,
692    };
693    Ok(KclValue::Segment {
694        value: Box::new(abstract_segment),
695    })
696}
697
698pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
699    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
700    let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
701    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
702    let construction: bool = construction_opt.unwrap_or(false);
703    let construction_ctor = construction_opt;
704
705    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
706        KclError::new_semantic(KclErrorDetails::new(
707            "start must be a 2D point".to_owned(),
708            vec![args.source_range],
709        ))
710    })?;
711    let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
712        KclError::new_semantic(KclErrorDetails::new(
713            "center must be a 2D point".to_owned(),
714            vec![args.source_range],
715        ))
716    })?;
717
718    let Some(UnsolvedExpr::Unknown(start_x)) = start_x_value.as_unsolved_expr() else {
719        return Err(KclError::new_semantic(KclErrorDetails::new(
720            "start x must be a sketch var".to_owned(),
721            vec![args.source_range],
722        )));
723    };
724    let Some(UnsolvedExpr::Unknown(start_y)) = start_y_value.as_unsolved_expr() else {
725        return Err(KclError::new_semantic(KclErrorDetails::new(
726            "start y must be a sketch var".to_owned(),
727            vec![args.source_range],
728        )));
729    };
730    let Some(UnsolvedExpr::Unknown(center_x)) = center_x_value.as_unsolved_expr() else {
731        return Err(KclError::new_semantic(KclErrorDetails::new(
732            "center x must be a sketch var".to_owned(),
733            vec![args.source_range],
734        )));
735    };
736    let Some(UnsolvedExpr::Unknown(center_y)) = center_y_value.as_unsolved_expr() else {
737        return Err(KclError::new_semantic(KclErrorDetails::new(
738            "center y must be a sketch var".to_owned(),
739            vec![args.source_range],
740        )));
741    };
742
743    let ctor = CircleCtor {
744        start: Point2d {
745            x: start_x_value.to_sketch_expr().ok_or_else(|| {
746                KclError::new_semantic(KclErrorDetails::new(
747                    "unable to convert numeric type to suffix".to_owned(),
748                    vec![args.source_range],
749                ))
750            })?,
751            y: start_y_value.to_sketch_expr().ok_or_else(|| {
752                KclError::new_semantic(KclErrorDetails::new(
753                    "unable to convert numeric type to suffix".to_owned(),
754                    vec![args.source_range],
755                ))
756            })?,
757        },
758        center: Point2d {
759            x: center_x_value.to_sketch_expr().ok_or_else(|| {
760                KclError::new_semantic(KclErrorDetails::new(
761                    "unable to convert numeric type to suffix".to_owned(),
762                    vec![args.source_range],
763                ))
764            })?,
765            y: center_y_value.to_sketch_expr().ok_or_else(|| {
766                KclError::new_semantic(KclErrorDetails::new(
767                    "unable to convert numeric type to suffix".to_owned(),
768                    vec![args.source_range],
769                ))
770            })?,
771        },
772        construction: construction_ctor,
773    };
774
775    // Order of ID generation is important.
776    let start_object_id = exec_state.next_object_id();
777    let center_object_id = exec_state.next_object_id();
778    let circle_object_id = exec_state.next_object_id();
779    let segment = UnsolvedSegment {
780        id: exec_state.next_uuid(),
781        object_id: circle_object_id,
782        kind: UnsolvedSegmentKind::Circle {
783            start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
784            center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
785            ctor: Box::new(ctor),
786            start_object_id,
787            center_object_id,
788            construction,
789        },
790        tag: None,
791        node_path: args.node_path.clone(),
792        meta: vec![args.source_range.into()],
793    };
794    #[cfg(feature = "artifact-graph")]
795    let optional_constraints = {
796        let start_object_id =
797            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
798        let center_object_id =
799            exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
800        let circle_object_id =
801            exec_state.add_placeholder_scene_object(circle_object_id, args.source_range, args.node_path.clone());
802
803        let mut optional_constraints = Vec::new();
804        if exec_state.segment_ids_edited_contains(&start_object_id)
805            || exec_state.segment_ids_edited_contains(&circle_object_id)
806        {
807            if let Some(start_x_var) = start_x_value.as_sketch_var() {
808                let x_initial_value = start_x_var.initial_value_to_solver_units(
809                    exec_state,
810                    args.source_range,
811                    "edited segment fixed constraint value",
812                )?;
813                optional_constraints.push(ezpz::Constraint::Fixed(
814                    start_x_var.id.to_constraint_id(args.source_range)?,
815                    x_initial_value.n,
816                ));
817            }
818            if let Some(start_y_var) = start_y_value.as_sketch_var() {
819                let y_initial_value = start_y_var.initial_value_to_solver_units(
820                    exec_state,
821                    args.source_range,
822                    "edited segment fixed constraint value",
823                )?;
824                optional_constraints.push(ezpz::Constraint::Fixed(
825                    start_y_var.id.to_constraint_id(args.source_range)?,
826                    y_initial_value.n,
827                ));
828            }
829        }
830        if exec_state.segment_ids_edited_contains(&center_object_id)
831            || exec_state.segment_ids_edited_contains(&circle_object_id)
832        {
833            if let Some(center_x_var) = center_x_value.as_sketch_var() {
834                let x_initial_value = center_x_var.initial_value_to_solver_units(
835                    exec_state,
836                    args.source_range,
837                    "edited segment fixed constraint value",
838                )?;
839                optional_constraints.push(ezpz::Constraint::Fixed(
840                    center_x_var.id.to_constraint_id(args.source_range)?,
841                    x_initial_value.n,
842                ));
843            }
844            if let Some(center_y_var) = center_y_value.as_sketch_var() {
845                let y_initial_value = center_y_var.initial_value_to_solver_units(
846                    exec_state,
847                    args.source_range,
848                    "edited segment fixed constraint value",
849                )?;
850                optional_constraints.push(ezpz::Constraint::Fixed(
851                    center_y_var.id.to_constraint_id(args.source_range)?,
852                    y_initial_value.n,
853                ));
854            }
855        }
856        optional_constraints
857    };
858
859    let Some(sketch_state) = exec_state.sketch_block_mut() else {
860        return Err(KclError::new_semantic(KclErrorDetails::new(
861            "circle() can only be used inside a sketch block".to_owned(),
862            vec![args.source_range],
863        )));
864    };
865    // Save the segment to be sent to the engine after solving.
866    sketch_state.needed_by_engine.push(segment.clone());
867
868    #[cfg(feature = "artifact-graph")]
869    sketch_state.solver_optional_constraints.extend(optional_constraints);
870
871    let meta = segment.meta.clone();
872    let abstract_segment = AbstractSegment {
873        repr: SegmentRepr::Unsolved {
874            segment: Box::new(segment),
875        },
876        meta,
877    };
878    Ok(KclValue::Segment {
879        value: Box::new(abstract_segment),
880    })
881}
882
883pub async fn coincident(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
884    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
885        "points",
886        &RuntimeType::Array(
887            Box::new(RuntimeType::Union(vec![RuntimeType::segment(), RuntimeType::point2d()])),
888            ArrayLen::Minimum(2),
889        ),
890        exec_state,
891    )?;
892    if points.len() > 2 {
893        return coincident_points(points, exec_state, args);
894    }
895    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
896        KclError::new_semantic(KclErrorDetails::new(
897            "must have two input points".to_owned(),
898            vec![args.source_range],
899        ))
900    })?;
901
902    let range = args.source_range;
903    match (&point0, &point1) {
904        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
905            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
906                return Err(KclError::new_semantic(KclErrorDetails::new(
907                    "first point must be an unsolved segment".to_owned(),
908                    vec![args.source_range],
909                )));
910            };
911            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
912                return Err(KclError::new_semantic(KclErrorDetails::new(
913                    "second point must be an unsolved segment".to_owned(),
914                    vec![args.source_range],
915                )));
916            };
917            match (&unsolved0.kind, &unsolved1.kind) {
918                (
919                    UnsolvedSegmentKind::Point { position: pos0, .. },
920                    UnsolvedSegmentKind::Point { position: pos1, .. },
921                ) => {
922                    let p0_x = &pos0[0];
923                    let p0_y = &pos0[1];
924                    match (p0_x, p0_y) {
925                        (UnsolvedExpr::Unknown(p0_x), UnsolvedExpr::Unknown(p0_y)) => {
926                            let p1_x = &pos1[0];
927                            let p1_y = &pos1[1];
928                            match (p1_x, p1_y) {
929                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
930                                    let constraint = SolverConstraint::PointsCoincident(
931                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
932                                            p0_x.to_constraint_id(range)?,
933                                            p0_y.to_constraint_id(range)?,
934                                        ),
935                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
936                                            p1_x.to_constraint_id(range)?,
937                                            p1_y.to_constraint_id(range)?,
938                                        ),
939                                    );
940                                    #[cfg(feature = "artifact-graph")]
941                                    let constraint_id = exec_state.next_object_id();
942                                    // Save the constraint to be used for solving.
943                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
944                                        return Err(KclError::new_semantic(KclErrorDetails::new(
945                                            "coincident() can only be used inside a sketch block".to_owned(),
946                                            vec![args.source_range],
947                                        )));
948                                    };
949                                    sketch_state.solver_constraints.push(constraint);
950                                    #[cfg(feature = "artifact-graph")]
951                                    {
952                                        let constraint = crate::front::Constraint::Coincident(Coincident {
953                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
954                                        });
955                                        sketch_state.sketch_constraints.push(constraint_id);
956                                        track_constraint(constraint_id, constraint, exec_state, &args);
957                                    }
958                                    Ok(KclValue::none())
959                                }
960                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
961                                    let p1_x = KclValue::Number {
962                                        value: p1_x.n,
963                                        ty: p1_x.ty,
964                                        meta: vec![args.source_range.into()],
965                                    };
966                                    let p1_y = KclValue::Number {
967                                        value: p1_y.n,
968                                        ty: p1_y.ty,
969                                        meta: vec![args.source_range.into()],
970                                    };
971                                    let (constraint_x, constraint_y) =
972                                        coincident_constraints_fixed(*p0_x, *p0_y, &p1_x, &p1_y, exec_state, &args)?;
973
974                                    #[cfg(feature = "artifact-graph")]
975                                    let constraint_id = exec_state.next_object_id();
976                                    // Save the constraint to be used for solving.
977                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
978                                        return Err(KclError::new_semantic(KclErrorDetails::new(
979                                            "coincident() can only be used inside a sketch block".to_owned(),
980                                            vec![args.source_range],
981                                        )));
982                                    };
983                                    sketch_state.solver_constraints.push(constraint_x);
984                                    sketch_state.solver_constraints.push(constraint_y);
985                                    #[cfg(feature = "artifact-graph")]
986                                    {
987                                        let constraint = crate::front::Constraint::Coincident(Coincident {
988                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
989                                        });
990                                        sketch_state.sketch_constraints.push(constraint_id);
991                                        track_constraint(constraint_id, constraint, exec_state, &args);
992                                    }
993                                    Ok(KclValue::none())
994                                }
995                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
996                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
997                                    // TODO: sketch-api: unimplemented
998                                    Err(KclError::new_semantic(KclErrorDetails::new(
999                                        "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(),
1000                                        vec![args.source_range],
1001                                    )))
1002                                }
1003                            }
1004                        }
1005                        (UnsolvedExpr::Known(p0_x), UnsolvedExpr::Known(p0_y)) => {
1006                            let p1_x = &pos1[0];
1007                            let p1_y = &pos1[1];
1008                            match (p1_x, p1_y) {
1009                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1010                                    let p0_x = KclValue::Number {
1011                                        value: p0_x.n,
1012                                        ty: p0_x.ty,
1013                                        meta: vec![args.source_range.into()],
1014                                    };
1015                                    let p0_y = KclValue::Number {
1016                                        value: p0_y.n,
1017                                        ty: p0_y.ty,
1018                                        meta: vec![args.source_range.into()],
1019                                    };
1020                                    let (constraint_x, constraint_y) =
1021                                        coincident_constraints_fixed(*p1_x, *p1_y, &p0_x, &p0_y, exec_state, &args)?;
1022
1023                                    #[cfg(feature = "artifact-graph")]
1024                                    let constraint_id = exec_state.next_object_id();
1025                                    // Save the constraint to be used for solving.
1026                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1027                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1028                                            "coincident() can only be used inside a sketch block".to_owned(),
1029                                            vec![args.source_range],
1030                                        )));
1031                                    };
1032                                    sketch_state.solver_constraints.push(constraint_x);
1033                                    sketch_state.solver_constraints.push(constraint_y);
1034                                    #[cfg(feature = "artifact-graph")]
1035                                    {
1036                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1037                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1038                                        });
1039                                        sketch_state.sketch_constraints.push(constraint_id);
1040                                        track_constraint(constraint_id, constraint, exec_state, &args);
1041                                    }
1042                                    Ok(KclValue::none())
1043                                }
1044                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1045                                    if *p0_x != *p1_x || *p0_y != *p1_y {
1046                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1047                                            "Coincident constraint between two fixed points failed since coordinates differ"
1048                                                .to_owned(),
1049                                            vec![args.source_range],
1050                                        )));
1051                                    }
1052                                    Ok(KclValue::none())
1053                                }
1054                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1055                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1056                                    // TODO: sketch-api: unimplemented
1057                                    Err(KclError::new_semantic(KclErrorDetails::new(
1058                                        "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(),
1059                                        vec![args.source_range],
1060                                    )))
1061                                }
1062                            }
1063                        }
1064                        (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1065                        | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1066                            // The segment is a point with one sketch var.
1067                            Err(KclError::new_semantic(KclErrorDetails::new(
1068                                "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(),
1069                                vec![args.source_range],
1070                            )))
1071                        }
1072                    }
1073                }
1074                // Point-Line or Line-Point case: create perpendicular distance constraint with distance 0
1075                (
1076                    UnsolvedSegmentKind::Point {
1077                        position: point_pos, ..
1078                    },
1079                    UnsolvedSegmentKind::Line {
1080                        start: line_start,
1081                        end: line_end,
1082                        ..
1083                    },
1084                )
1085                | (
1086                    UnsolvedSegmentKind::Line {
1087                        start: line_start,
1088                        end: line_end,
1089                        ..
1090                    },
1091                    UnsolvedSegmentKind::Point {
1092                        position: point_pos, ..
1093                    },
1094                ) => {
1095                    let point_x = &point_pos[0];
1096                    let point_y = &point_pos[1];
1097                    match (point_x, point_y) {
1098                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1099                            // Extract line start and end coordinates
1100                            let (start_x, start_y) = (&line_start[0], &line_start[1]);
1101                            let (end_x, end_y) = (&line_end[0], &line_end[1]);
1102
1103                            match (start_x, start_y, end_x, end_y) {
1104                                (
1105                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1106                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1107                                ) => {
1108                                    let point = DatumPoint::new_xy(
1109                                        point_x.to_constraint_id(range)?,
1110                                        point_y.to_constraint_id(range)?,
1111                                    );
1112                                    let line_segment = DatumLineSegment::new(
1113                                        DatumPoint::new_xy(sx.to_constraint_id(range)?, sy.to_constraint_id(range)?),
1114                                        DatumPoint::new_xy(ex.to_constraint_id(range)?, ey.to_constraint_id(range)?),
1115                                    );
1116                                    let constraint = SolverConstraint::PointLineDistance(point, line_segment, 0.0);
1117
1118                                    #[cfg(feature = "artifact-graph")]
1119                                    let constraint_id = exec_state.next_object_id();
1120
1121                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1122                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1123                                            "coincident() can only be used inside a sketch block".to_owned(),
1124                                            vec![args.source_range],
1125                                        )));
1126                                    };
1127                                    sketch_state.solver_constraints.push(constraint);
1128                                    #[cfg(feature = "artifact-graph")]
1129                                    {
1130                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1131                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1132                                        });
1133                                        sketch_state.sketch_constraints.push(constraint_id);
1134                                        track_constraint(constraint_id, constraint, exec_state, &args);
1135                                    }
1136                                    Ok(KclValue::none())
1137                                }
1138                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1139                                    "Line segment endpoints must be sketch variables for point-segment coincident constraint".to_owned(),
1140                                    vec![args.source_range],
1141                                ))),
1142                            }
1143                        }
1144                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1145                            "Point coordinates must be sketch variables for point-segment coincident constraint"
1146                                .to_owned(),
1147                            vec![args.source_range],
1148                        ))),
1149                    }
1150                }
1151                // Point-Arc or Arc-Point case: create PointArcCoincident constraint
1152                (
1153                    UnsolvedSegmentKind::Point {
1154                        position: point_pos, ..
1155                    },
1156                    UnsolvedSegmentKind::Arc {
1157                        start: arc_start,
1158                        end: arc_end,
1159                        center: arc_center,
1160                        ..
1161                    },
1162                )
1163                | (
1164                    UnsolvedSegmentKind::Arc {
1165                        start: arc_start,
1166                        end: arc_end,
1167                        center: arc_center,
1168                        ..
1169                    },
1170                    UnsolvedSegmentKind::Point {
1171                        position: point_pos, ..
1172                    },
1173                ) => {
1174                    let point_x = &point_pos[0];
1175                    let point_y = &point_pos[1];
1176                    match (point_x, point_y) {
1177                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1178                            // Extract arc center, start, and end coordinates
1179                            let (center_x, center_y) = (&arc_center[0], &arc_center[1]);
1180                            let (start_x, start_y) = (&arc_start[0], &arc_start[1]);
1181                            let (end_x, end_y) = (&arc_end[0], &arc_end[1]);
1182
1183                            match (center_x, center_y, start_x, start_y, end_x, end_y) {
1184                                (
1185                                    UnsolvedExpr::Unknown(cx), UnsolvedExpr::Unknown(cy),
1186                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1187                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1188                                ) => {
1189                                    let point = DatumPoint::new_xy(
1190                                        point_x.to_constraint_id(range)?,
1191                                        point_y.to_constraint_id(range)?,
1192                                    );
1193                                    let circular_arc = DatumCircularArc {
1194                                        center: DatumPoint::new_xy(
1195                                            cx.to_constraint_id(range)?,
1196                                            cy.to_constraint_id(range)?,
1197                                        ),
1198                                        start: DatumPoint::new_xy(
1199                                            sx.to_constraint_id(range)?,
1200                                            sy.to_constraint_id(range)?,
1201                                        ),
1202                                        end: DatumPoint::new_xy(
1203                                            ex.to_constraint_id(range)?,
1204                                            ey.to_constraint_id(range)?,
1205                                        ),
1206                                    };
1207                                    let constraint = SolverConstraint::PointArcCoincident(circular_arc, point);
1208
1209                                    #[cfg(feature = "artifact-graph")]
1210                                    let constraint_id = exec_state.next_object_id();
1211
1212                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1213                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1214                                            "coincident() can only be used inside a sketch block".to_owned(),
1215                                            vec![args.source_range],
1216                                        )));
1217                                    };
1218                                    sketch_state.solver_constraints.push(constraint);
1219                                    #[cfg(feature = "artifact-graph")]
1220                                    {
1221                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1222                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1223                                        });
1224                                        sketch_state.sketch_constraints.push(constraint_id);
1225                                        track_constraint(constraint_id, constraint, exec_state, &args);
1226                                    }
1227                                    Ok(KclValue::none())
1228                                }
1229                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1230                                    "Arc center, start, and end points must be sketch variables for point-arc coincident constraint".to_owned(),
1231                                    vec![args.source_range],
1232                                ))),
1233                            }
1234                        }
1235                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1236                            "Point coordinates must be sketch variables for point-arc coincident constraint".to_owned(),
1237                            vec![args.source_range],
1238                        ))),
1239                    }
1240                }
1241                // Point-Circle or Circle-Point case: constrain point-to-center distance
1242                // to equal the circle radius.
1243                (
1244                    UnsolvedSegmentKind::Point {
1245                        position: point_pos, ..
1246                    },
1247                    UnsolvedSegmentKind::Circle {
1248                        start: circle_start,
1249                        center: circle_center,
1250                        ..
1251                    },
1252                )
1253                | (
1254                    UnsolvedSegmentKind::Circle {
1255                        start: circle_start,
1256                        center: circle_center,
1257                        ..
1258                    },
1259                    UnsolvedSegmentKind::Point {
1260                        position: point_pos, ..
1261                    },
1262                ) => {
1263                    let point_x = &point_pos[0];
1264                    let point_y = &point_pos[1];
1265                    match (point_x, point_y) {
1266                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1267                            // Extract circle center and start coordinates.
1268                            let (center_x, center_y) = (&circle_center[0], &circle_center[1]);
1269                            let (start_x, start_y) = (&circle_start[0], &circle_start[1]);
1270
1271                            match (center_x, center_y, start_x, start_y) {
1272                                (
1273                                    UnsolvedExpr::Unknown(cx),
1274                                    UnsolvedExpr::Unknown(cy),
1275                                    UnsolvedExpr::Unknown(sx),
1276                                    UnsolvedExpr::Unknown(sy),
1277                                ) => {
1278                                    let point_radius_line = DatumLineSegment::new(
1279                                        DatumPoint::new_xy(
1280                                            cx.to_constraint_id(range)?,
1281                                            cy.to_constraint_id(range)?,
1282                                        ),
1283                                        DatumPoint::new_xy(
1284                                            point_x.to_constraint_id(range)?,
1285                                            point_y.to_constraint_id(range)?,
1286                                        ),
1287                                    );
1288                                    let circle_radius_line = DatumLineSegment::new(
1289                                        DatumPoint::new_xy(
1290                                            cx.to_constraint_id(range)?,
1291                                            cy.to_constraint_id(range)?,
1292                                        ),
1293                                        DatumPoint::new_xy(
1294                                            sx.to_constraint_id(range)?,
1295                                            sy.to_constraint_id(range)?,
1296                                        ),
1297                                    );
1298                                    let constraint =
1299                                        SolverConstraint::LinesEqualLength(point_radius_line, circle_radius_line);
1300
1301                                    #[cfg(feature = "artifact-graph")]
1302                                    let constraint_id = exec_state.next_object_id();
1303
1304                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1305                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1306                                            "coincident() can only be used inside a sketch block".to_owned(),
1307                                            vec![args.source_range],
1308                                        )));
1309                                    };
1310                                    sketch_state.solver_constraints.push(constraint);
1311                                    #[cfg(feature = "artifact-graph")]
1312                                    {
1313                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1314                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1315                                        });
1316                                        sketch_state.sketch_constraints.push(constraint_id);
1317                                        track_constraint(constraint_id, constraint, exec_state, &args);
1318                                    }
1319                                    Ok(KclValue::none())
1320                                }
1321                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1322                                    "Circle start and center points must be sketch variables for point-circle coincident constraint".to_owned(),
1323                                    vec![args.source_range],
1324                                ))),
1325                            }
1326                        }
1327                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1328                            "Point coordinates must be sketch variables for point-circle coincident constraint"
1329                                .to_owned(),
1330                            vec![args.source_range],
1331                        ))),
1332                    }
1333                }
1334                // Line-Line case: create parallel constraint and perpendicular distance of zero
1335                (
1336                    UnsolvedSegmentKind::Line {
1337                        start: line0_start,
1338                        end: line0_end,
1339                        ..
1340                    },
1341                    UnsolvedSegmentKind::Line {
1342                        start: line1_start,
1343                        end: line1_end,
1344                        ..
1345                    },
1346                ) => {
1347                    // Extract line coordinates
1348                    let (line0_start_x, line0_start_y) = (&line0_start[0], &line0_start[1]);
1349                    let (line0_end_x, line0_end_y) = (&line0_end[0], &line0_end[1]);
1350                    let (line1_start_x, line1_start_y) = (&line1_start[0], &line1_start[1]);
1351                    let (line1_end_x, line1_end_y) = (&line1_end[0], &line1_end[1]);
1352
1353                    match (
1354                        line0_start_x,
1355                        line0_start_y,
1356                        line0_end_x,
1357                        line0_end_y,
1358                        line1_start_x,
1359                        line1_start_y,
1360                        line1_end_x,
1361                        line1_end_y,
1362                    ) {
1363                        (
1364                            UnsolvedExpr::Unknown(l0_sx),
1365                            UnsolvedExpr::Unknown(l0_sy),
1366                            UnsolvedExpr::Unknown(l0_ex),
1367                            UnsolvedExpr::Unknown(l0_ey),
1368                            UnsolvedExpr::Unknown(l1_sx),
1369                            UnsolvedExpr::Unknown(l1_sy),
1370                            UnsolvedExpr::Unknown(l1_ex),
1371                            UnsolvedExpr::Unknown(l1_ey),
1372                        ) => {
1373                            // Create line segments for the solver
1374                            let line0_segment = DatumLineSegment::new(
1375                                DatumPoint::new_xy(l0_sx.to_constraint_id(range)?, l0_sy.to_constraint_id(range)?),
1376                                DatumPoint::new_xy(l0_ex.to_constraint_id(range)?, l0_ey.to_constraint_id(range)?),
1377                            );
1378                            let line1_segment = DatumLineSegment::new(
1379                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?),
1380                                DatumPoint::new_xy(l1_ex.to_constraint_id(range)?, l1_ey.to_constraint_id(range)?),
1381                            );
1382
1383                            // Create parallel constraint
1384                            let parallel_constraint =
1385                                SolverConstraint::LinesAtAngle(line0_segment, line1_segment, AngleKind::Parallel);
1386
1387                            // Create perpendicular distance constraint from first line to start point of second line
1388                            let point_on_line1 =
1389                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?);
1390                            let distance_constraint =
1391                                SolverConstraint::PointLineDistance(point_on_line1, line0_segment, 0.0);
1392
1393                            #[cfg(feature = "artifact-graph")]
1394                            let constraint_id = exec_state.next_object_id();
1395
1396                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1397                                return Err(KclError::new_semantic(KclErrorDetails::new(
1398                                    "coincident() can only be used inside a sketch block".to_owned(),
1399                                    vec![args.source_range],
1400                                )));
1401                            };
1402                            // Push both constraints to achieve collinearity
1403                            sketch_state.solver_constraints.push(parallel_constraint);
1404                            sketch_state.solver_constraints.push(distance_constraint);
1405                            #[cfg(feature = "artifact-graph")]
1406                            {
1407                                let constraint = crate::front::Constraint::Coincident(Coincident {
1408                                    segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1409                                });
1410                                sketch_state.sketch_constraints.push(constraint_id);
1411                                track_constraint(constraint_id, constraint, exec_state, &args);
1412                            }
1413                            Ok(KclValue::none())
1414                        }
1415                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1416                            "Line segment endpoints must be sketch variables for line-line coincident constraint"
1417                                .to_owned(),
1418                            vec![args.source_range],
1419                        ))),
1420                    }
1421                }
1422                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1423                    format!(
1424                        "coincident supports point-point, point-segment, or segment-segment; found {:?} and {:?}",
1425                        &unsolved0.kind, &unsolved1.kind
1426                    ),
1427                    vec![args.source_range],
1428                ))),
1429            }
1430        }
1431        // One argument is a Segment and the other is a Point2d literal.
1432        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1433        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1434            let Some(pt) = <[TyF64; 2]>::from_kcl_val(point2d) else {
1435                return Err(KclError::new_semantic(KclErrorDetails::new(
1436                    "Expected a Segment or Point2d (e.g. [1mm, 2mm])".to_owned(),
1437                    vec![args.source_range],
1438                )));
1439            };
1440            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1441                return Err(KclError::new_semantic(KclErrorDetails::new(
1442                    "segment must be an unsolved segment".to_owned(),
1443                    vec![args.source_range],
1444                )));
1445            };
1446            match &unsolved.kind {
1447                UnsolvedSegmentKind::Point { position, .. } => {
1448                    let p_x = &position[0];
1449                    let p_y = &position[1];
1450                    match (p_x, p_y) {
1451                        (UnsolvedExpr::Unknown(p_x), UnsolvedExpr::Unknown(p_y)) => {
1452                            let pt_x = KclValue::Number {
1453                                value: pt[0].n,
1454                                ty: pt[0].ty,
1455                                meta: vec![args.source_range.into()],
1456                            };
1457                            let pt_y = KclValue::Number {
1458                                value: pt[1].n,
1459                                ty: pt[1].ty,
1460                                meta: vec![args.source_range.into()],
1461                            };
1462                            let (constraint_x, constraint_y) =
1463                                coincident_constraints_fixed(*p_x, *p_y, &pt_x, &pt_y, exec_state, &args)?;
1464
1465                            #[cfg(feature = "artifact-graph")]
1466                            let constraint_id = exec_state.next_object_id();
1467                            #[cfg(feature = "artifact-graph")]
1468                            let coincident_segments = coincident_segments_for_segment_and_point2d(
1469                                unsolved.object_id,
1470                                point2d,
1471                                matches!((&point0, &point1), (KclValue::Segment { .. }, _)),
1472                            );
1473                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1474                                return Err(KclError::new_semantic(KclErrorDetails::new(
1475                                    "coincident() can only be used inside a sketch block".to_owned(),
1476                                    vec![args.source_range],
1477                                )));
1478                            };
1479                            sketch_state.solver_constraints.push(constraint_x);
1480                            sketch_state.solver_constraints.push(constraint_y);
1481                            #[cfg(feature = "artifact-graph")]
1482                            {
1483                                let constraint = crate::front::Constraint::Coincident(Coincident {
1484                                    segments: coincident_segments,
1485                                });
1486                                sketch_state.sketch_constraints.push(constraint_id);
1487                                track_constraint(constraint_id, constraint, exec_state, &args);
1488                            }
1489                            Ok(KclValue::none())
1490                        }
1491                        (UnsolvedExpr::Known(known_x), UnsolvedExpr::Known(known_y)) => {
1492                            let pt_x_val = normalize_to_solver_distance_unit(
1493                                &KclValue::Number {
1494                                    value: pt[0].n,
1495                                    ty: pt[0].ty,
1496                                    meta: vec![args.source_range.into()],
1497                                },
1498                                args.source_range,
1499                                exec_state,
1500                                "coincident constraint value",
1501                            )?;
1502                            let pt_y_val = normalize_to_solver_distance_unit(
1503                                &KclValue::Number {
1504                                    value: pt[1].n,
1505                                    ty: pt[1].ty,
1506                                    meta: vec![args.source_range.into()],
1507                                },
1508                                args.source_range,
1509                                exec_state,
1510                                "coincident constraint value",
1511                            )?;
1512                            let Some(pt_x) = pt_x_val.as_ty_f64() else {
1513                                return Err(KclError::new_semantic(KclErrorDetails::new(
1514                                    "Expected number for Point2d x coordinate".to_owned(),
1515                                    vec![args.source_range],
1516                                )));
1517                            };
1518                            let Some(pt_y) = pt_y_val.as_ty_f64() else {
1519                                return Err(KclError::new_semantic(KclErrorDetails::new(
1520                                    "Expected number for Point2d y coordinate".to_owned(),
1521                                    vec![args.source_range],
1522                                )));
1523                            };
1524                            let known_x_val = normalize_to_solver_distance_unit(
1525                                &KclValue::Number {
1526                                    value: known_x.n,
1527                                    ty: known_x.ty,
1528                                    meta: vec![args.source_range.into()],
1529                                },
1530                                args.source_range,
1531                                exec_state,
1532                                "coincident constraint value",
1533                            )?;
1534                            let Some(known_x_f) = known_x_val.as_ty_f64() else {
1535                                return Err(KclError::new_semantic(KclErrorDetails::new(
1536                                    "Expected number for known x coordinate".to_owned(),
1537                                    vec![args.source_range],
1538                                )));
1539                            };
1540                            let known_y_val = normalize_to_solver_distance_unit(
1541                                &KclValue::Number {
1542                                    value: known_y.n,
1543                                    ty: known_y.ty,
1544                                    meta: vec![args.source_range.into()],
1545                                },
1546                                args.source_range,
1547                                exec_state,
1548                                "coincident constraint value",
1549                            )?;
1550                            let Some(known_y_f) = known_y_val.as_ty_f64() else {
1551                                return Err(KclError::new_semantic(KclErrorDetails::new(
1552                                    "Expected number for known y coordinate".to_owned(),
1553                                    vec![args.source_range],
1554                                )));
1555                            };
1556                            if known_x_f.n != pt_x.n || known_y_f.n != pt_y.n {
1557                                return Err(KclError::new_semantic(KclErrorDetails::new(
1558                                    "Coincident constraint between two fixed points failed since coordinates differ"
1559                                        .to_owned(),
1560                                    vec![args.source_range],
1561                                )));
1562                            }
1563                            Ok(KclValue::none())
1564                        }
1565                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1566                            "Point coordinates must have consistent known/unknown status for coincident constraint"
1567                                .to_owned(),
1568                            vec![args.source_range],
1569                        ))),
1570                    }
1571                }
1572                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1573                    "A Point2d can only be constrained coincident with a point segment, not a line or arc".to_owned(),
1574                    vec![args.source_range],
1575                ))),
1576            }
1577        }
1578        // Both arguments are Point2d literals -- just verify equality.
1579        _ => {
1580            let pt0 = <[TyF64; 2]>::from_kcl_val(&point0);
1581            let pt1 = <[TyF64; 2]>::from_kcl_val(&point1);
1582            match (pt0, pt1) {
1583                (Some(a), Some(b)) => {
1584                    // Normalize both to solver units and compare.
1585                    let a_x = normalize_to_solver_distance_unit(
1586                        &KclValue::Number {
1587                            value: a[0].n,
1588                            ty: a[0].ty,
1589                            meta: vec![args.source_range.into()],
1590                        },
1591                        args.source_range,
1592                        exec_state,
1593                        "coincident constraint value",
1594                    )?;
1595                    let a_y = normalize_to_solver_distance_unit(
1596                        &KclValue::Number {
1597                            value: a[1].n,
1598                            ty: a[1].ty,
1599                            meta: vec![args.source_range.into()],
1600                        },
1601                        args.source_range,
1602                        exec_state,
1603                        "coincident constraint value",
1604                    )?;
1605                    let b_x = normalize_to_solver_distance_unit(
1606                        &KclValue::Number {
1607                            value: b[0].n,
1608                            ty: b[0].ty,
1609                            meta: vec![args.source_range.into()],
1610                        },
1611                        args.source_range,
1612                        exec_state,
1613                        "coincident constraint value",
1614                    )?;
1615                    let b_y = normalize_to_solver_distance_unit(
1616                        &KclValue::Number {
1617                            value: b[1].n,
1618                            ty: b[1].ty,
1619                            meta: vec![args.source_range.into()],
1620                        },
1621                        args.source_range,
1622                        exec_state,
1623                        "coincident constraint value",
1624                    )?;
1625                    if a_x.as_ty_f64().map(|v| v.n) != b_x.as_ty_f64().map(|v| v.n)
1626                        || a_y.as_ty_f64().map(|v| v.n) != b_y.as_ty_f64().map(|v| v.n)
1627                    {
1628                        return Err(KclError::new_semantic(KclErrorDetails::new(
1629                            "Coincident constraint between two fixed points failed since coordinates differ".to_owned(),
1630                            vec![args.source_range],
1631                        )));
1632                    }
1633                    Ok(KclValue::none())
1634                }
1635                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1636                    "All inputs must be Segments or Point2d values".to_owned(),
1637                    vec![args.source_range],
1638                ))),
1639            }
1640        }
1641    }
1642}
1643
1644fn coincident_points(
1645    point_values: Vec<KclValue>,
1646    exec_state: &mut ExecState,
1647    args: Args,
1648) -> Result<KclValue, KclError> {
1649    if point_values.len() < 2 {
1650        return Err(KclError::new_semantic(KclErrorDetails::new(
1651            "coincident() point list must contain at least two points".to_owned(),
1652            vec![args.source_range],
1653        )));
1654    }
1655
1656    // For every point return either a fixed point or a variable point
1657    let points = point_values
1658        .iter()
1659        .map(|point| extract_multi_coincident_point(point, args.source_range))
1660        .collect::<Result<Vec<_>, _>>()?;
1661
1662    #[cfg(feature = "artifact-graph")]
1663    let constraint_segments = points.iter().map(|point| point.constraint_segment).collect::<Vec<_>>();
1664
1665    let mut variable_points = Vec::new();
1666    let mut fixed_points = Vec::new();
1667    for point in points {
1668        match point.point {
1669            PointToAlign::Variable { x, y } => variable_points.push([x, y]),
1670            PointToAlign::Fixed { x, y } => fixed_points.push([x, y]),
1671        }
1672    }
1673
1674    let mut solver_constraints = Vec::with_capacity(point_values.len().saturating_sub(1) * 2);
1675    if let Some((anchor_fixed, remaining_fixed_points)) = fixed_points.split_first() {
1676        // A fixed point becomes the shared target location for every variable point.
1677        if remaining_fixed_points
1678            .iter()
1679            .any(|point| !fixed_points_match(point, anchor_fixed))
1680        {
1681            return Err(KclError::new_semantic(KclErrorDetails::new(
1682                "coincident() with more than two inputs can include at most one fixed point location".to_owned(),
1683                vec![args.source_range],
1684            )));
1685        }
1686
1687        let anchor_x = ty_f64_to_kcl_value(anchor_fixed[0].clone(), args.source_range);
1688        let anchor_y = ty_f64_to_kcl_value(anchor_fixed[1].clone(), args.source_range);
1689        for point in variable_points {
1690            let (constraint_x, constraint_y) =
1691                coincident_constraints_fixed(point[0], point[1], &anchor_x, &anchor_y, exec_state, &args)?;
1692            solver_constraints.push(constraint_x);
1693            solver_constraints.push(constraint_y);
1694        }
1695    } else {
1696        // With only variable points, anchor everything to the first point.
1697        let mut points = variable_points.into_iter();
1698        let first_point = points.next().ok_or_else(|| {
1699            KclError::new_semantic(KclErrorDetails::new(
1700                "coincident() point list must contain at least two points".to_owned(),
1701                vec![args.source_range],
1702            ))
1703        })?;
1704        let anchor = datum_point(first_point, args.source_range)?;
1705        for point in points {
1706            let solver_point = datum_point(point, args.source_range)?;
1707            solver_constraints.push(SolverConstraint::PointsCoincident(anchor, solver_point));
1708        }
1709    }
1710
1711    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1712        return Err(KclError::new_semantic(KclErrorDetails::new(
1713            "coincident() can only be used inside a sketch block".to_owned(),
1714            vec![args.source_range],
1715        )));
1716    };
1717    sketch_state.solver_constraints.extend(solver_constraints);
1718
1719    #[cfg(feature = "artifact-graph")]
1720    {
1721        // Keep one artifact-graph coincident constraint even though the solver sees multiple relations.
1722        let constraint_id = exec_state.next_object_id();
1723        let Some(sketch_state) = exec_state.sketch_block_mut() else {
1724            debug_assert!(false, "Constraint created outside a sketch block");
1725            return Ok(KclValue::none());
1726        };
1727        sketch_state.sketch_constraints.push(constraint_id);
1728        let constraint = Constraint::Coincident(Coincident {
1729            segments: constraint_segments,
1730        });
1731        track_constraint(constraint_id, constraint, exec_state, &args);
1732    }
1733
1734    Ok(KclValue::none())
1735}
1736
1737fn extract_multi_coincident_point(
1738    input: &KclValue,
1739    source_range: crate::SourceRange,
1740) -> Result<CoincidentPointInput, KclError> {
1741    // Normalize each multi-input item into either a fixed point or solver-backed point vars.
1742    match input {
1743        KclValue::Segment { value: segment } => {
1744            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
1745                return Err(KclError::new_semantic(KclErrorDetails::new(
1746                    "coincident() with more than two inputs only supports unsolved points or ORIGIN".to_owned(),
1747                    vec![source_range],
1748                )));
1749            };
1750            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
1751                return Err(KclError::new_semantic(KclErrorDetails::new(
1752                    format!(
1753                        "coincident() with more than two inputs only supports points or ORIGIN, but one item is {}",
1754                        unsolved.kind.human_friendly_kind_with_article()
1755                    ),
1756                    vec![source_range],
1757                )));
1758            };
1759            match (&position[0], &position[1]) {
1760                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(CoincidentPointInput {
1761                    point: PointToAlign::Fixed {
1762                        x: x.to_owned(),
1763                        y: y.to_owned(),
1764                    },
1765                    #[cfg(feature = "artifact-graph")]
1766                    constraint_segment: unsolved.object_id.into(),
1767                }),
1768                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(CoincidentPointInput {
1769                    point: PointToAlign::Variable { x: *x, y: *y },
1770                    #[cfg(feature = "artifact-graph")]
1771                    constraint_segment: unsolved.object_id.into(),
1772                }),
1773                // Mixed points not supported
1774                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..))
1775                | (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => Err(KclError::new_semantic(
1776                    KclErrorDetails::new(
1777                        "coincident() with more than two inputs requires each point to be fully fixed or fully variable"
1778                            .to_owned(),
1779                        vec![source_range],
1780                    ),
1781                )),
1782            }
1783        }
1784        point if point2d_is_origin(point) => {
1785            let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point) else {
1786                debug_assert!(false, "Origin literal should coerce to Point2d");
1787                return Err(KclError::new_internal(KclErrorDetails::new(
1788                    "Origin literal could not be converted to a point".to_owned(),
1789                    vec![source_range],
1790                )));
1791            };
1792            Ok(CoincidentPointInput {
1793                point: PointToAlign::Fixed { x, y },
1794                #[cfg(feature = "artifact-graph")]
1795                constraint_segment: ConstraintSegment::ORIGIN,
1796            })
1797        }
1798        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1799            "coincident() with more than two inputs only supports points and ORIGIN".to_owned(),
1800            vec![source_range],
1801        ))),
1802    }
1803}
1804
1805#[derive(Debug, Clone)]
1806struct CoincidentPointInput {
1807    point: PointToAlign,
1808    #[cfg(feature = "artifact-graph")]
1809    constraint_segment: ConstraintSegment,
1810}
1811
1812fn fixed_points_match(a: &[TyF64; 2], b: &[TyF64; 2]) -> bool {
1813    a[0].to_mm() == b[0].to_mm() && a[1].to_mm() == b[1].to_mm()
1814}
1815
1816fn ty_f64_to_kcl_value(value: TyF64, source_range: crate::SourceRange) -> KclValue {
1817    KclValue::Number {
1818        value: value.n,
1819        ty: value.ty,
1820        meta: vec![source_range.into()],
1821    }
1822}
1823
1824#[cfg(feature = "artifact-graph")]
1825fn track_constraint(constraint_id: ObjectId, constraint: Constraint, exec_state: &mut ExecState, args: &Args) {
1826    let sketch_id = {
1827        let Some(sketch_state) = exec_state.sketch_block_mut() else {
1828            debug_assert!(false, "Constraint created outside a sketch block");
1829            return;
1830        };
1831        sketch_state.sketch_id
1832    };
1833    let Some(sketch_id) = sketch_id else {
1834        debug_assert!(false, "Constraint created without a sketch id");
1835        return;
1836    };
1837    let artifact_id = exec_state.next_artifact_id();
1838    exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
1839        id: artifact_id,
1840        sketch_id,
1841        constraint_id,
1842        constraint_type: SketchBlockConstraintType::from(&constraint),
1843        code_ref: CodeRef::placeholder(args.source_range),
1844    }));
1845    exec_state.add_scene_object(
1846        Object {
1847            id: constraint_id,
1848            kind: ObjectKind::Constraint { constraint },
1849            label: Default::default(),
1850            comments: Default::default(),
1851            artifact_id,
1852            source: SourceRef::new(args.source_range, args.node_path.clone()),
1853        },
1854        args.source_range,
1855    );
1856}
1857
1858/// Order of points has been erased when calling this function.
1859fn coincident_constraints_fixed(
1860    p0_x: SketchVarId,
1861    p0_y: SketchVarId,
1862    p1_x: &KclValue,
1863    p1_y: &KclValue,
1864    exec_state: &mut ExecState,
1865    args: &Args,
1866) -> Result<(ezpz::Constraint, ezpz::Constraint), KclError> {
1867    let p1_x_number_value =
1868        normalize_to_solver_distance_unit(p1_x, p1_x.into(), exec_state, "coincident constraint value")?;
1869    let p1_y_number_value =
1870        normalize_to_solver_distance_unit(p1_y, p1_y.into(), exec_state, "coincident constraint value")?;
1871    let Some(p1_x) = p1_x_number_value.as_ty_f64() else {
1872        let message = format!(
1873            "Expected number after coercion, but found {}",
1874            p1_x_number_value.human_friendly_type()
1875        );
1876        debug_assert!(false, "{}", &message);
1877        return Err(KclError::new_internal(KclErrorDetails::new(
1878            message,
1879            vec![args.source_range],
1880        )));
1881    };
1882    let Some(p1_y) = p1_y_number_value.as_ty_f64() else {
1883        let message = format!(
1884            "Expected number after coercion, but found {}",
1885            p1_y_number_value.human_friendly_type()
1886        );
1887        debug_assert!(false, "{}", &message);
1888        return Err(KclError::new_internal(KclErrorDetails::new(
1889            message,
1890            vec![args.source_range],
1891        )));
1892    };
1893    let constraint_x = SolverConstraint::Fixed(p0_x.to_constraint_id(args.source_range)?, p1_x.n);
1894    let constraint_y = SolverConstraint::Fixed(p0_y.to_constraint_id(args.source_range)?, p1_y.n);
1895    Ok((constraint_x, constraint_y))
1896}
1897
1898pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1899    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1900        "points",
1901        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
1902        exec_state,
1903    )?;
1904    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
1905        KclError::new_semantic(KclErrorDetails::new(
1906            "must have two input points".to_owned(),
1907            vec![args.source_range],
1908        ))
1909    })?;
1910
1911    match (&point0, &point1) {
1912        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1913            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1914                return Err(KclError::new_semantic(KclErrorDetails::new(
1915                    "first point must be an unsolved segment".to_owned(),
1916                    vec![args.source_range],
1917                )));
1918            };
1919            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1920                return Err(KclError::new_semantic(KclErrorDetails::new(
1921                    "second point must be an unsolved segment".to_owned(),
1922                    vec![args.source_range],
1923                )));
1924            };
1925            match (&unsolved0.kind, &unsolved1.kind) {
1926                (
1927                    UnsolvedSegmentKind::Point { position: pos0, .. },
1928                    UnsolvedSegmentKind::Point { position: pos1, .. },
1929                ) => {
1930                    // Both segments are points. Create a distance constraint
1931                    // between them.
1932                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
1933                        (
1934                            UnsolvedExpr::Unknown(p0_x),
1935                            UnsolvedExpr::Unknown(p0_y),
1936                            UnsolvedExpr::Unknown(p1_x),
1937                            UnsolvedExpr::Unknown(p1_y),
1938                        ) => {
1939                            // All coordinates are sketch vars. Proceed.
1940                            let sketch_constraint = SketchConstraint {
1941                                kind: SketchConstraintKind::Distance {
1942                                    points: [
1943                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1944                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
1945                                            object_id: unsolved0.object_id,
1946                                        }),
1947                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1948                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
1949                                            object_id: unsolved1.object_id,
1950                                        }),
1951                                    ],
1952                                },
1953                                meta: vec![args.source_range.into()],
1954                            };
1955                            Ok(KclValue::SketchConstraint {
1956                                value: Box::new(sketch_constraint),
1957                            })
1958                        }
1959                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1960                            "unimplemented: distance() arguments must be all sketch vars in all coordinates".to_owned(),
1961                            vec![args.source_range],
1962                        ))),
1963                    }
1964                }
1965                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1966                    "distance() arguments must be unsolved points".to_owned(),
1967                    vec![args.source_range],
1968                ))),
1969            }
1970        }
1971        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1972        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1973            if !point2d_is_origin(point2d) {
1974                return Err(KclError::new_semantic(KclErrorDetails::new(
1975                    "distance() Point2d arguments must be ORIGIN".to_owned(),
1976                    vec![args.source_range],
1977                )));
1978            }
1979
1980            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1981                return Err(KclError::new_semantic(KclErrorDetails::new(
1982                    "segment must be an unsolved segment".to_owned(),
1983                    vec![args.source_range],
1984                )));
1985            };
1986            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
1987                return Err(KclError::new_semantic(KclErrorDetails::new(
1988                    "distance() arguments must be unsolved points or ORIGIN".to_owned(),
1989                    vec![args.source_range],
1990                )));
1991            };
1992            match (&position[0], &position[1]) {
1993                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1994                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1995                        vars: crate::front::Point2d {
1996                            x: *point_x,
1997                            y: *point_y,
1998                        },
1999                        object_id: unsolved.object_id,
2000                    });
2001                    let points = if matches!((&point0, &point1), (KclValue::Segment { .. }, _)) {
2002                        [point, ConstrainablePoint2dOrOrigin::Origin]
2003                    } else {
2004                        [ConstrainablePoint2dOrOrigin::Origin, point]
2005                    };
2006                    Ok(KclValue::SketchConstraint {
2007                        value: Box::new(SketchConstraint {
2008                            kind: SketchConstraintKind::Distance { points },
2009                            meta: vec![args.source_range.into()],
2010                        }),
2011                    })
2012                }
2013                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2014                    "unimplemented: distance() point arguments must be sketch vars in all coordinates".to_owned(),
2015                    vec![args.source_range],
2016                ))),
2017            }
2018        }
2019        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2020            "distance() arguments must be point segments or ORIGIN".to_owned(),
2021            vec![args.source_range],
2022        ))),
2023    }
2024}
2025
2026/// Helper function to create a radius or diameter constraint from a circular segment.
2027/// Used by both radius() and diameter() functions.
2028fn create_circular_radius_constraint(
2029    segment: KclValue,
2030    constraint_kind: fn([ConstrainablePoint2d; 2]) -> SketchConstraintKind,
2031    source_range: crate::SourceRange,
2032) -> Result<SketchConstraint, KclError> {
2033    // Create a dummy constraint to get its name for error messages
2034    let dummy_constraint = constraint_kind([
2035        ConstrainablePoint2d {
2036            vars: crate::front::Point2d {
2037                x: SketchVarId(0),
2038                y: SketchVarId(0),
2039            },
2040            object_id: ObjectId(0),
2041        },
2042        ConstrainablePoint2d {
2043            vars: crate::front::Point2d {
2044                x: SketchVarId(0),
2045                y: SketchVarId(0),
2046            },
2047            object_id: ObjectId(0),
2048        },
2049    ]);
2050    let function_name = dummy_constraint.name();
2051
2052    let KclValue::Segment { value: seg } = segment else {
2053        return Err(KclError::new_semantic(KclErrorDetails::new(
2054            format!("{}() argument must be a segment", function_name),
2055            vec![source_range],
2056        )));
2057    };
2058    let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2059        return Err(KclError::new_semantic(KclErrorDetails::new(
2060            "segment must be unsolved".to_owned(),
2061            vec![source_range],
2062        )));
2063    };
2064    match &unsolved.kind {
2065        UnsolvedSegmentKind::Arc {
2066            center,
2067            start,
2068            center_object_id,
2069            start_object_id,
2070            ..
2071        }
2072        | UnsolvedSegmentKind::Circle {
2073            center,
2074            start,
2075            center_object_id,
2076            start_object_id,
2077            ..
2078        } => {
2079            // Extract center and start point coordinates
2080            match (&center[0], &center[1], &start[0], &start[1]) {
2081                (
2082                    UnsolvedExpr::Unknown(center_x),
2083                    UnsolvedExpr::Unknown(center_y),
2084                    UnsolvedExpr::Unknown(start_x),
2085                    UnsolvedExpr::Unknown(start_y),
2086                ) => {
2087                    // All coordinates are sketch vars. Create constraint.
2088                    let sketch_constraint = SketchConstraint {
2089                        kind: constraint_kind([
2090                            ConstrainablePoint2d {
2091                                vars: crate::front::Point2d {
2092                                    x: *center_x,
2093                                    y: *center_y,
2094                                },
2095                                object_id: *center_object_id,
2096                            },
2097                            ConstrainablePoint2d {
2098                                vars: crate::front::Point2d {
2099                                    x: *start_x,
2100                                    y: *start_y,
2101                                },
2102                                object_id: *start_object_id,
2103                            },
2104                        ]),
2105                        meta: vec![source_range.into()],
2106                    };
2107                    Ok(sketch_constraint)
2108                }
2109                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2110                    format!(
2111                        "unimplemented: {}() arc or circle segment must have all sketch vars in all coordinates",
2112                        function_name
2113                    ),
2114                    vec![source_range],
2115                ))),
2116            }
2117        }
2118        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2119            format!("{}() argument must be an arc or circle segment", function_name),
2120            vec![source_range],
2121        ))),
2122    }
2123}
2124
2125pub async fn radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2126    let segment: KclValue =
2127        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2128
2129    create_circular_radius_constraint(
2130        segment,
2131        |points| SketchConstraintKind::Radius { points },
2132        args.source_range,
2133    )
2134    .map(|constraint| KclValue::SketchConstraint {
2135        value: Box::new(constraint),
2136    })
2137}
2138
2139pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2140    let segment: KclValue =
2141        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2142
2143    create_circular_radius_constraint(
2144        segment,
2145        |points| SketchConstraintKind::Diameter { points },
2146        args.source_range,
2147    )
2148    .map(|constraint| KclValue::SketchConstraint {
2149        value: Box::new(constraint),
2150    })
2151}
2152
2153pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2154    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2155        "points",
2156        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2157        exec_state,
2158    )?;
2159    let [p1, p2] = points.as_slice() else {
2160        return Err(KclError::new_semantic(KclErrorDetails::new(
2161            "must have two input points".to_owned(),
2162            vec![args.source_range],
2163        )));
2164    };
2165    match (p1, p2) {
2166        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2167            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2168                return Err(KclError::new_semantic(KclErrorDetails::new(
2169                    "first point must be an unsolved segment".to_owned(),
2170                    vec![args.source_range],
2171                )));
2172            };
2173            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2174                return Err(KclError::new_semantic(KclErrorDetails::new(
2175                    "second point must be an unsolved segment".to_owned(),
2176                    vec![args.source_range],
2177                )));
2178            };
2179            match (&unsolved0.kind, &unsolved1.kind) {
2180                (
2181                    UnsolvedSegmentKind::Point { position: pos0, .. },
2182                    UnsolvedSegmentKind::Point { position: pos1, .. },
2183                ) => {
2184                    // Both segments are points. Create a horizontal distance constraint
2185                    // between them.
2186                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2187                        (
2188                            UnsolvedExpr::Unknown(p0_x),
2189                            UnsolvedExpr::Unknown(p0_y),
2190                            UnsolvedExpr::Unknown(p1_x),
2191                            UnsolvedExpr::Unknown(p1_y),
2192                        ) => {
2193                            // All coordinates are sketch vars. Proceed.
2194                            let sketch_constraint = SketchConstraint {
2195                                kind: SketchConstraintKind::HorizontalDistance {
2196                                    points: [
2197                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2198                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2199                                            object_id: unsolved0.object_id,
2200                                        }),
2201                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2202                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2203                                            object_id: unsolved1.object_id,
2204                                        }),
2205                                    ],
2206                                },
2207                                meta: vec![args.source_range.into()],
2208                            };
2209                            Ok(KclValue::SketchConstraint {
2210                                value: Box::new(sketch_constraint),
2211                            })
2212                        }
2213                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2214                            "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
2215                                .to_owned(),
2216                            vec![args.source_range],
2217                        ))),
2218                    }
2219                }
2220                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2221                    "horizontalDistance() arguments must be unsolved points".to_owned(),
2222                    vec![args.source_range],
2223                ))),
2224            }
2225        }
2226        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2227        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2228            if !point2d_is_origin(point2d) {
2229                return Err(KclError::new_semantic(KclErrorDetails::new(
2230                    "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
2231                    vec![args.source_range],
2232                )));
2233            }
2234
2235            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2236                return Err(KclError::new_semantic(KclErrorDetails::new(
2237                    "segment must be an unsolved segment".to_owned(),
2238                    vec![args.source_range],
2239                )));
2240            };
2241            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2242                return Err(KclError::new_semantic(KclErrorDetails::new(
2243                    "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2244                    vec![args.source_range],
2245                )));
2246            };
2247            match (&position[0], &position[1]) {
2248                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2249                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2250                        vars: crate::front::Point2d {
2251                            x: *point_x,
2252                            y: *point_y,
2253                        },
2254                        object_id: unsolved.object_id,
2255                    });
2256                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2257                        [point, ConstrainablePoint2dOrOrigin::Origin]
2258                    } else {
2259                        [ConstrainablePoint2dOrOrigin::Origin, point]
2260                    };
2261                    Ok(KclValue::SketchConstraint {
2262                        value: Box::new(SketchConstraint {
2263                            kind: SketchConstraintKind::HorizontalDistance { points },
2264                            meta: vec![args.source_range.into()],
2265                        }),
2266                    })
2267                }
2268                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2269                    "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
2270                        .to_owned(),
2271                    vec![args.source_range],
2272                ))),
2273            }
2274        }
2275        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2276            "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
2277            vec![args.source_range],
2278        ))),
2279    }
2280}
2281
2282pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2283    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2284        "points",
2285        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2286        exec_state,
2287    )?;
2288    let [p1, p2] = points.as_slice() else {
2289        return Err(KclError::new_semantic(KclErrorDetails::new(
2290            "must have two input points".to_owned(),
2291            vec![args.source_range],
2292        )));
2293    };
2294    match (p1, p2) {
2295        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2296            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2297                return Err(KclError::new_semantic(KclErrorDetails::new(
2298                    "first point must be an unsolved segment".to_owned(),
2299                    vec![args.source_range],
2300                )));
2301            };
2302            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2303                return Err(KclError::new_semantic(KclErrorDetails::new(
2304                    "second point must be an unsolved segment".to_owned(),
2305                    vec![args.source_range],
2306                )));
2307            };
2308            match (&unsolved0.kind, &unsolved1.kind) {
2309                (
2310                    UnsolvedSegmentKind::Point { position: pos0, .. },
2311                    UnsolvedSegmentKind::Point { position: pos1, .. },
2312                ) => {
2313                    // Both segments are points. Create a vertical distance constraint
2314                    // between them.
2315                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2316                        (
2317                            UnsolvedExpr::Unknown(p0_x),
2318                            UnsolvedExpr::Unknown(p0_y),
2319                            UnsolvedExpr::Unknown(p1_x),
2320                            UnsolvedExpr::Unknown(p1_y),
2321                        ) => {
2322                            // All coordinates are sketch vars. Proceed.
2323                            let sketch_constraint = SketchConstraint {
2324                                kind: SketchConstraintKind::VerticalDistance {
2325                                    points: [
2326                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2327                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2328                                            object_id: unsolved0.object_id,
2329                                        }),
2330                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2331                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2332                                            object_id: unsolved1.object_id,
2333                                        }),
2334                                    ],
2335                                },
2336                                meta: vec![args.source_range.into()],
2337                            };
2338                            Ok(KclValue::SketchConstraint {
2339                                value: Box::new(sketch_constraint),
2340                            })
2341                        }
2342                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2343                            "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
2344                                .to_owned(),
2345                            vec![args.source_range],
2346                        ))),
2347                    }
2348                }
2349                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2350                    "verticalDistance() arguments must be unsolved points".to_owned(),
2351                    vec![args.source_range],
2352                ))),
2353            }
2354        }
2355        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2356            if !point2d_is_origin(point2d) {
2357                return Err(KclError::new_semantic(KclErrorDetails::new(
2358                    "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
2359                    vec![args.source_range],
2360                )));
2361            }
2362
2363            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2364                return Err(KclError::new_semantic(KclErrorDetails::new(
2365                    "segment must be an unsolved segment".to_owned(),
2366                    vec![args.source_range],
2367                )));
2368            };
2369            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2370                return Err(KclError::new_semantic(KclErrorDetails::new(
2371                    "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2372                    vec![args.source_range],
2373                )));
2374            };
2375            match (&position[0], &position[1]) {
2376                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2377                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2378                        vars: crate::front::Point2d {
2379                            x: *point_x,
2380                            y: *point_y,
2381                        },
2382                        object_id: unsolved.object_id,
2383                    });
2384                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2385                        [point, ConstrainablePoint2dOrOrigin::Origin]
2386                    } else {
2387                        [ConstrainablePoint2dOrOrigin::Origin, point]
2388                    };
2389                    Ok(KclValue::SketchConstraint {
2390                        value: Box::new(SketchConstraint {
2391                            kind: SketchConstraintKind::VerticalDistance { points },
2392                            meta: vec![args.source_range.into()],
2393                        }),
2394                    })
2395                }
2396                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2397                    "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
2398                        .to_owned(),
2399                    vec![args.source_range],
2400                ))),
2401            }
2402        }
2403        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2404            "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
2405            vec![args.source_range],
2406        ))),
2407    }
2408}
2409
2410pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2411    #[derive(Clone, Copy)]
2412    struct ConstrainableLine {
2413        solver_line: DatumLineSegment,
2414        #[cfg(feature = "artifact-graph")]
2415        object_id: ObjectId,
2416    }
2417
2418    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2419        "lines",
2420        &RuntimeType::Array(
2421            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2422            ArrayLen::Minimum(2),
2423        ),
2424        exec_state,
2425    )?;
2426    let range = args.source_range;
2427    let constrainable_lines: Vec<ConstrainableLine> = lines
2428        .iter()
2429        .map(|line| {
2430            let KclValue::Segment { value: segment } = line else {
2431                return Err(KclError::new_semantic(KclErrorDetails::new(
2432                    "line argument must be a Segment".to_owned(),
2433                    vec![args.source_range],
2434                )));
2435            };
2436            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2437                return Err(KclError::new_internal(KclErrorDetails::new(
2438                    "line must be an unsolved Segment".to_owned(),
2439                    vec![args.source_range],
2440                )));
2441            };
2442            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2443                return Err(KclError::new_semantic(KclErrorDetails::new(
2444                    "line argument must be a line, no other type of Segment".to_owned(),
2445                    vec![args.source_range],
2446                )));
2447            };
2448            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2449                return Err(KclError::new_semantic(KclErrorDetails::new(
2450                    "line's start x coordinate must be a var".to_owned(),
2451                    vec![args.source_range],
2452                )));
2453            };
2454            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2455                return Err(KclError::new_semantic(KclErrorDetails::new(
2456                    "line's start y coordinate must be a var".to_owned(),
2457                    vec![args.source_range],
2458                )));
2459            };
2460            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2461                return Err(KclError::new_semantic(KclErrorDetails::new(
2462                    "line's end x coordinate must be a var".to_owned(),
2463                    vec![args.source_range],
2464                )));
2465            };
2466            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2467                return Err(KclError::new_semantic(KclErrorDetails::new(
2468                    "line's end y coordinate must be a var".to_owned(),
2469                    vec![args.source_range],
2470                )));
2471            };
2472
2473            let solver_line_p0 =
2474                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2475            let solver_line_p1 =
2476                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2477
2478            Ok(ConstrainableLine {
2479                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2480                #[cfg(feature = "artifact-graph")]
2481                object_id: unsolved.object_id,
2482            })
2483        })
2484        .collect::<Result<_, _>>()?;
2485
2486    #[cfg(feature = "artifact-graph")]
2487    let constraint_id = exec_state.next_object_id();
2488    // Save the constraint to be used for solving.
2489    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2490        return Err(KclError::new_semantic(KclErrorDetails::new(
2491            "equalLength() can only be used inside a sketch block".to_owned(),
2492            vec![args.source_range],
2493        )));
2494    };
2495    let first_line = constrainable_lines[0];
2496    for line in constrainable_lines.iter().skip(1) {
2497        sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
2498            first_line.solver_line,
2499            line.solver_line,
2500        ));
2501    }
2502    #[cfg(feature = "artifact-graph")]
2503    {
2504        let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
2505            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
2506        });
2507        sketch_state.sketch_constraints.push(constraint_id);
2508        track_constraint(constraint_id, constraint, exec_state, &args);
2509    }
2510    Ok(KclValue::none())
2511}
2512
2513fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
2514    Ok(DatumPoint::new_xy(
2515        coords[0].to_constraint_id(range)?,
2516        coords[1].to_constraint_id(range)?,
2517    ))
2518}
2519
2520fn sketch_var_initial_value(
2521    sketch_vars: &[KclValue],
2522    id: SketchVarId,
2523    exec_state: &mut ExecState,
2524    range: crate::SourceRange,
2525) -> Result<f64, KclError> {
2526    sketch_vars
2527        .get(id.0)
2528        .and_then(KclValue::as_sketch_var)
2529        .map(|sketch_var| {
2530            sketch_var
2531                .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
2532                .map(|value| value.n)
2533        })
2534        .transpose()?
2535        .ok_or_else(|| {
2536            KclError::new_internal(KclErrorDetails::new(
2537                format!("Missing sketch variable initial value for id {}", id.0),
2538                vec![range],
2539            ))
2540        })
2541}
2542
2543fn radius_guess(
2544    sketch_vars: &[KclValue],
2545    center: [SketchVarId; 2],
2546    point: [SketchVarId; 2],
2547    exec_state: &mut ExecState,
2548    range: crate::SourceRange,
2549) -> Result<f64, KclError> {
2550    let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
2551        - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
2552    let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
2553        - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
2554    Ok(libm::hypot(dx, dy))
2555}
2556
2557pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2558    #[derive(Debug, Clone, Copy)]
2559    struct RadiusInputVars {
2560        center: [SketchVarId; 2],
2561        start: [SketchVarId; 2],
2562        end: Option<[SketchVarId; 2]>,
2563    }
2564
2565    #[derive(Debug, Clone, Copy)]
2566    enum EqualRadiusInput {
2567        Radius(RadiusInputVars),
2568    }
2569
2570    fn extract_equal_radius_input(
2571        segment_value: &KclValue,
2572        range: crate::SourceRange,
2573    ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
2574        let KclValue::Segment { value: segment } = segment_value else {
2575            return Err(KclError::new_semantic(KclErrorDetails::new(
2576                format!(
2577                    "equalRadius() arguments must be segments but found {}",
2578                    segment_value.human_friendly_type()
2579                ),
2580                vec![range],
2581            )));
2582        };
2583        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2584            return Err(KclError::new_semantic(KclErrorDetails::new(
2585                "equalRadius() arguments must be unsolved segments".to_owned(),
2586                vec![range],
2587            )));
2588        };
2589        match &unsolved.kind {
2590            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2591                let (
2592                    UnsolvedExpr::Unknown(center_x),
2593                    UnsolvedExpr::Unknown(center_y),
2594                    UnsolvedExpr::Unknown(start_x),
2595                    UnsolvedExpr::Unknown(start_y),
2596                    UnsolvedExpr::Unknown(end_x),
2597                    UnsolvedExpr::Unknown(end_y),
2598                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2599                else {
2600                    return Err(KclError::new_semantic(KclErrorDetails::new(
2601                        "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
2602                        vec![range],
2603                    )));
2604                };
2605                Ok((
2606                    EqualRadiusInput::Radius(RadiusInputVars {
2607                        center: [*center_x, *center_y],
2608                        start: [*start_x, *start_y],
2609                        end: Some([*end_x, *end_y]),
2610                    }),
2611                    unsolved.object_id,
2612                ))
2613            }
2614            UnsolvedSegmentKind::Circle { center, start, .. } => {
2615                let (
2616                    UnsolvedExpr::Unknown(center_x),
2617                    UnsolvedExpr::Unknown(center_y),
2618                    UnsolvedExpr::Unknown(start_x),
2619                    UnsolvedExpr::Unknown(start_y),
2620                ) = (&center[0], &center[1], &start[0], &start[1])
2621                else {
2622                    return Err(KclError::new_semantic(KclErrorDetails::new(
2623                        "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
2624                        vec![range],
2625                    )));
2626                };
2627                Ok((
2628                    EqualRadiusInput::Radius(RadiusInputVars {
2629                        center: [*center_x, *center_y],
2630                        start: [*start_x, *start_y],
2631                        end: None,
2632                    }),
2633                    unsolved.object_id,
2634                ))
2635            }
2636            other => Err(KclError::new_semantic(KclErrorDetails::new(
2637                format!(
2638                    "equalRadius() currently supports only arc and circle segments, you provided {}",
2639                    other.human_friendly_kind_with_article()
2640                ),
2641                vec![range],
2642            ))),
2643        }
2644    }
2645
2646    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2647        "input",
2648        &RuntimeType::Array(
2649            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2650            ArrayLen::Minimum(2),
2651        ),
2652        exec_state,
2653    )?;
2654    let range = args.source_range;
2655
2656    let extracted_input = input
2657        .iter()
2658        .map(|segment_value| extract_equal_radius_input(segment_value, range))
2659        .collect::<Result<Vec<_>, _>>()?;
2660    let radius_inputs: Vec<RadiusInputVars> = extracted_input
2661        .iter()
2662        .map(|(equal_radius_input, _)| match equal_radius_input {
2663            EqualRadiusInput::Radius(radius_input) => *radius_input,
2664        })
2665        .collect();
2666    #[cfg(feature = "artifact-graph")]
2667    let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
2668
2669    let sketch_var_ty = solver_numeric_type(exec_state);
2670    #[cfg(feature = "artifact-graph")]
2671    let constraint_id = exec_state.next_object_id();
2672
2673    let sketch_vars = {
2674        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2675            return Err(KclError::new_semantic(KclErrorDetails::new(
2676                "equalRadius() can only be used inside a sketch block".to_owned(),
2677                vec![range],
2678            )));
2679        };
2680        sketch_state.sketch_vars.clone()
2681    };
2682
2683    let radius_initial_value = radius_guess(
2684        &sketch_vars,
2685        radius_inputs[0].center,
2686        radius_inputs[0].start,
2687        exec_state,
2688        range,
2689    )?;
2690
2691    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2692        return Err(KclError::new_semantic(KclErrorDetails::new(
2693            "equalRadius() can only be used inside a sketch block".to_owned(),
2694            vec![range],
2695        )));
2696    };
2697    let radius_id = sketch_state.next_sketch_var_id();
2698    sketch_state.sketch_vars.push(KclValue::SketchVar {
2699        value: Box::new(crate::execution::SketchVar {
2700            id: radius_id,
2701            initial_value: radius_initial_value,
2702            ty: sketch_var_ty,
2703            meta: vec![],
2704        }),
2705    });
2706    let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2707
2708    for radius_input in radius_inputs {
2709        let center = datum_point(radius_input.center, range)?;
2710        let start = datum_point(radius_input.start, range)?;
2711        sketch_state
2712            .solver_constraints
2713            .push(SolverConstraint::DistanceVar(start, center, radius));
2714        if let Some(end) = radius_input.end {
2715            let end = datum_point(end, range)?;
2716            sketch_state
2717                .solver_constraints
2718                .push(SolverConstraint::DistanceVar(end, center, radius));
2719        }
2720    }
2721
2722    #[cfg(feature = "artifact-graph")]
2723    {
2724        let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
2725            input: input_object_ids,
2726        });
2727        sketch_state.sketch_constraints.push(constraint_id);
2728        track_constraint(constraint_id, constraint, exec_state, &args);
2729    }
2730
2731    Ok(KclValue::none())
2732}
2733
2734pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2735    #[derive(Debug, Clone, Copy)]
2736    struct ConstrainableLineVars {
2737        start: [SketchVarId; 2],
2738        end: [SketchVarId; 2],
2739    }
2740
2741    #[derive(Debug, Clone, Copy)]
2742    struct ConstrainableCircularVars {
2743        center: [SketchVarId; 2],
2744        start: [SketchVarId; 2],
2745        end: Option<[SketchVarId; 2]>,
2746    }
2747
2748    #[derive(Debug, Clone, Copy)]
2749    enum TangentInput {
2750        Line(ConstrainableLineVars),
2751        Circular(ConstrainableCircularVars),
2752    }
2753
2754    fn extract_tangent_input(
2755        segment_value: &KclValue,
2756        range: crate::SourceRange,
2757    ) -> Result<(TangentInput, ObjectId), KclError> {
2758        let KclValue::Segment { value: segment } = segment_value else {
2759            return Err(KclError::new_semantic(KclErrorDetails::new(
2760                "tangent() arguments must be segments".to_owned(),
2761                vec![range],
2762            )));
2763        };
2764        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2765            return Err(KclError::new_semantic(KclErrorDetails::new(
2766                "tangent() arguments must be unsolved segments".to_owned(),
2767                vec![range],
2768            )));
2769        };
2770        match &unsolved.kind {
2771            UnsolvedSegmentKind::Line { start, end, .. } => {
2772                let (
2773                    UnsolvedExpr::Unknown(start_x),
2774                    UnsolvedExpr::Unknown(start_y),
2775                    UnsolvedExpr::Unknown(end_x),
2776                    UnsolvedExpr::Unknown(end_y),
2777                ) = (&start[0], &start[1], &end[0], &end[1])
2778                else {
2779                    return Err(KclError::new_semantic(KclErrorDetails::new(
2780                        "line coordinates must be sketch vars for tangent()".to_owned(),
2781                        vec![range],
2782                    )));
2783                };
2784                Ok((
2785                    TangentInput::Line(ConstrainableLineVars {
2786                        start: [*start_x, *start_y],
2787                        end: [*end_x, *end_y],
2788                    }),
2789                    unsolved.object_id,
2790                ))
2791            }
2792            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2793                let (
2794                    UnsolvedExpr::Unknown(center_x),
2795                    UnsolvedExpr::Unknown(center_y),
2796                    UnsolvedExpr::Unknown(start_x),
2797                    UnsolvedExpr::Unknown(start_y),
2798                    UnsolvedExpr::Unknown(end_x),
2799                    UnsolvedExpr::Unknown(end_y),
2800                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2801                else {
2802                    return Err(KclError::new_semantic(KclErrorDetails::new(
2803                        "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
2804                        vec![range],
2805                    )));
2806                };
2807                Ok((
2808                    TangentInput::Circular(ConstrainableCircularVars {
2809                        center: [*center_x, *center_y],
2810                        start: [*start_x, *start_y],
2811                        end: Some([*end_x, *end_y]),
2812                    }),
2813                    unsolved.object_id,
2814                ))
2815            }
2816            UnsolvedSegmentKind::Circle { center, start, .. } => {
2817                let (
2818                    UnsolvedExpr::Unknown(center_x),
2819                    UnsolvedExpr::Unknown(center_y),
2820                    UnsolvedExpr::Unknown(start_x),
2821                    UnsolvedExpr::Unknown(start_y),
2822                ) = (&center[0], &center[1], &start[0], &start[1])
2823                else {
2824                    return Err(KclError::new_semantic(KclErrorDetails::new(
2825                        "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
2826                        vec![range],
2827                    )));
2828                };
2829                Ok((
2830                    TangentInput::Circular(ConstrainableCircularVars {
2831                        center: [*center_x, *center_y],
2832                        start: [*start_x, *start_y],
2833                        end: None,
2834                    }),
2835                    unsolved.object_id,
2836                ))
2837            }
2838            _ => Err(KclError::new_semantic(KclErrorDetails::new(
2839                "tangent() supports only line, arc, and circle segments".to_owned(),
2840                vec![range],
2841            ))),
2842        }
2843    }
2844
2845    fn point_initial_position(
2846        sketch_vars: &[KclValue],
2847        point: [SketchVarId; 2],
2848        exec_state: &mut ExecState,
2849        range: crate::SourceRange,
2850    ) -> Result<[f64; 2], KclError> {
2851        Ok([
2852            sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
2853            sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
2854        ])
2855    }
2856
2857    fn canonicalize_line_for_tangent(
2858        sketch_vars: &[KclValue],
2859        line: ConstrainableLineVars,
2860        arc_center: [SketchVarId; 2],
2861        exec_state: &mut ExecState,
2862        range: crate::SourceRange,
2863    ) -> Result<ConstrainableLineVars, KclError> {
2864        let [sx, sy] = point_initial_position(sketch_vars, line.start, exec_state, range)?;
2865        let [ex, ey] = point_initial_position(sketch_vars, line.end, exec_state, range)?;
2866        let [cx, cy] = point_initial_position(sketch_vars, arc_center, exec_state, range)?;
2867
2868        // Canonicalize the line orientation so LineTangentToCircle sees the arc
2869        // center on its non-negative side regardless of how the user ordered the
2870        // line endpoints in KCL.
2871        let signed_side = (ex - sx) * (cy - sy) - (ey - sy) * (cx - sx);
2872        if signed_side < -1e-9 {
2873            Ok(ConstrainableLineVars {
2874                start: line.end,
2875                end: line.start,
2876            })
2877        } else {
2878            Ok(line)
2879        }
2880    }
2881
2882    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2883        "input",
2884        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2885        exec_state,
2886    )?;
2887    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
2888        KclError::new_semantic(KclErrorDetails::new(
2889            "tangent() requires exactly 2 input segments".to_owned(),
2890            vec![args.source_range],
2891        ))
2892    })?;
2893    let range = args.source_range;
2894    let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
2895    let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
2896    #[cfg(not(feature = "artifact-graph"))]
2897    let _ = (input0_object_id, input1_object_id);
2898
2899    enum TangentCase {
2900        LineCircular(ConstrainableLineVars, ConstrainableCircularVars),
2901        CircularCircular(ConstrainableCircularVars, ConstrainableCircularVars),
2902    }
2903    let tangent_case = match (input0, input1) {
2904        (TangentInput::Line(line), TangentInput::Circular(circular))
2905        | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
2906        (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
2907            TangentCase::CircularCircular(circular0, circular1)
2908        }
2909        (TangentInput::Line(_), TangentInput::Line(_)) => {
2910            return Err(KclError::new_semantic(KclErrorDetails::new(
2911                "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
2912                vec![range],
2913            )));
2914        }
2915    };
2916
2917    let sketch_var_ty = solver_numeric_type(exec_state);
2918    #[cfg(feature = "artifact-graph")]
2919    let constraint_id = exec_state.next_object_id();
2920
2921    let sketch_vars = {
2922        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2923            return Err(KclError::new_semantic(KclErrorDetails::new(
2924                "tangent() can only be used inside a sketch block".to_owned(),
2925                vec![range],
2926            )));
2927        };
2928        sketch_state.sketch_vars.clone()
2929    };
2930
2931    // Hidden radius vars. Empty metadata keeps them out of source write-back.
2932    match tangent_case {
2933        TangentCase::LineCircular(line, circular) => {
2934            let canonical_line = canonicalize_line_for_tangent(&sketch_vars, line, circular.center, exec_state, range)?;
2935            let line_p0 = datum_point(canonical_line.start, range)?;
2936            let line_p1 = datum_point(canonical_line.end, range)?;
2937            let line_datum = DatumLineSegment::new(line_p0, line_p1);
2938
2939            let center = datum_point(circular.center, range)?;
2940            let circular_start = datum_point(circular.start, range)?;
2941            let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
2942            let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
2943            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2944                return Err(KclError::new_semantic(KclErrorDetails::new(
2945                    "tangent() can only be used inside a sketch block".to_owned(),
2946                    vec![range],
2947                )));
2948            };
2949            let radius_id = sketch_state.next_sketch_var_id();
2950            sketch_state.sketch_vars.push(KclValue::SketchVar {
2951                value: Box::new(crate::execution::SketchVar {
2952                    id: radius_id,
2953                    initial_value: radius_initial_value,
2954                    ty: sketch_var_ty,
2955                    meta: vec![],
2956                }),
2957            });
2958            let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2959            let circle = DatumCircle { center, radius };
2960
2961            // Tangency decomposition for Line/circular segment:
2962            // 1) Introduce a hidden radius variable r for the segment's underlying circle.
2963            // 2) Keep the segment's defining points on that circle with DistanceVar(point, center, r).
2964            // 3) Canonicalize the solver line orientation so endpoint order
2965            //    doesn't change the tangent branch.
2966            // 4) Apply the native LineTangentToCircle solver constraint.
2967            sketch_state
2968                .solver_constraints
2969                .push(SolverConstraint::DistanceVar(circular_start, center, radius));
2970            if let Some(circular_end) = circular_end {
2971                sketch_state
2972                    .solver_constraints
2973                    .push(SolverConstraint::DistanceVar(circular_end, center, radius));
2974            }
2975            sketch_state
2976                .solver_constraints
2977                .push(SolverConstraint::LineTangentToCircle(line_datum, circle));
2978        }
2979        TangentCase::CircularCircular(circular0, circular1) => {
2980            let center0 = datum_point(circular0.center, range)?;
2981            let start0 = datum_point(circular0.start, range)?;
2982            let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
2983            let radius0_initial_value =
2984                radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
2985            let center1 = datum_point(circular1.center, range)?;
2986            let start1 = datum_point(circular1.start, range)?;
2987            let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
2988            let radius1_initial_value =
2989                radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
2990            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2991                return Err(KclError::new_semantic(KclErrorDetails::new(
2992                    "tangent() can only be used inside a sketch block".to_owned(),
2993                    vec![range],
2994                )));
2995            };
2996            let radius0_id = sketch_state.next_sketch_var_id();
2997            sketch_state.sketch_vars.push(KclValue::SketchVar {
2998                value: Box::new(crate::execution::SketchVar {
2999                    id: radius0_id,
3000                    initial_value: radius0_initial_value,
3001                    ty: sketch_var_ty,
3002                    meta: vec![],
3003                }),
3004            });
3005            let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
3006            let circle0 = DatumCircle {
3007                center: center0,
3008                radius: radius0,
3009            };
3010
3011            let radius1_id = sketch_state.next_sketch_var_id();
3012            sketch_state.sketch_vars.push(KclValue::SketchVar {
3013                value: Box::new(crate::execution::SketchVar {
3014                    id: radius1_id,
3015                    initial_value: radius1_initial_value,
3016                    ty: sketch_var_ty,
3017                    meta: vec![],
3018                }),
3019            });
3020            let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
3021            let circle1 = DatumCircle {
3022                center: center1,
3023                radius: radius1,
3024            };
3025
3026            // Tangency decomposition for circular segment/circular segment:
3027            // 1) Introduce one hidden radius variable per arc.
3028            // 2) Keep each segment's defining points on its corresponding circle.
3029            // 3) Apply the native CircleTangentToCircle solver constraint.
3030            sketch_state
3031                .solver_constraints
3032                .push(SolverConstraint::DistanceVar(start0, center0, radius0));
3033            if let Some(end0) = end0 {
3034                sketch_state
3035                    .solver_constraints
3036                    .push(SolverConstraint::DistanceVar(end0, center0, radius0));
3037            }
3038            sketch_state
3039                .solver_constraints
3040                .push(SolverConstraint::DistanceVar(start1, center1, radius1));
3041            if let Some(end1) = end1 {
3042                sketch_state
3043                    .solver_constraints
3044                    .push(SolverConstraint::DistanceVar(end1, center1, radius1));
3045            }
3046            sketch_state
3047                .solver_constraints
3048                .push(SolverConstraint::CircleTangentToCircle(circle0, circle1));
3049        }
3050    }
3051
3052    #[cfg(feature = "artifact-graph")]
3053    {
3054        let constraint = crate::front::Constraint::Tangent(Tangent {
3055            input: vec![input0_object_id, input1_object_id],
3056        });
3057        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3058            return Err(KclError::new_semantic(KclErrorDetails::new(
3059                "tangent() can only be used inside a sketch block".to_owned(),
3060                vec![range],
3061            )));
3062        };
3063        sketch_state.sketch_constraints.push(constraint_id);
3064        track_constraint(constraint_id, constraint, exec_state, &args);
3065    }
3066
3067    Ok(KclValue::none())
3068}
3069
3070#[derive(Debug, Clone, Copy)]
3071pub(crate) enum LinesAtAngleKind {
3072    Parallel,
3073    Perpendicular,
3074}
3075
3076impl LinesAtAngleKind {
3077    pub fn to_function_name(self) -> &'static str {
3078        match self {
3079            LinesAtAngleKind::Parallel => "parallel",
3080            LinesAtAngleKind::Perpendicular => "perpendicular",
3081        }
3082    }
3083
3084    fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
3085        match self {
3086            LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
3087            LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
3088        }
3089    }
3090
3091    #[cfg(feature = "artifact-graph")]
3092    fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
3093        match self {
3094            LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
3095            LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
3096        }
3097    }
3098}
3099
3100/// Convert between two different libraries with similar angle representations
3101#[expect(unused)]
3102fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
3103    kcmc::shared::Angle::from_degrees(angle.to_degrees())
3104}
3105
3106/// Convert between two different libraries with similar angle representations
3107#[expect(unused)]
3108fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
3109    ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
3110}
3111
3112pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3113    #[derive(Clone, Copy)]
3114    struct ConstrainableLine {
3115        solver_line: DatumLineSegment,
3116        #[cfg(feature = "artifact-graph")]
3117        object_id: ObjectId,
3118    }
3119
3120    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3121        "lines",
3122        &RuntimeType::Array(
3123            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3124            ArrayLen::Minimum(2),
3125        ),
3126        exec_state,
3127    )?;
3128    let range = args.source_range;
3129    let constrainable_lines: Vec<ConstrainableLine> = lines
3130        .iter()
3131        .map(|line| {
3132            let KclValue::Segment { value: segment } = line else {
3133                return Err(KclError::new_semantic(KclErrorDetails::new(
3134                    "line argument must be a Segment".to_owned(),
3135                    vec![args.source_range],
3136                )));
3137            };
3138            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3139                return Err(KclError::new_internal(KclErrorDetails::new(
3140                    "line must be an unsolved Segment".to_owned(),
3141                    vec![args.source_range],
3142                )));
3143            };
3144            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3145                return Err(KclError::new_semantic(KclErrorDetails::new(
3146                    "line argument must be a line, no other type of Segment".to_owned(),
3147                    vec![args.source_range],
3148                )));
3149            };
3150            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
3151                return Err(KclError::new_semantic(KclErrorDetails::new(
3152                    "line's start x coordinate must be a var".to_owned(),
3153                    vec![args.source_range],
3154                )));
3155            };
3156            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
3157                return Err(KclError::new_semantic(KclErrorDetails::new(
3158                    "line's start y coordinate must be a var".to_owned(),
3159                    vec![args.source_range],
3160                )));
3161            };
3162            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
3163                return Err(KclError::new_semantic(KclErrorDetails::new(
3164                    "line's end x coordinate must be a var".to_owned(),
3165                    vec![args.source_range],
3166                )));
3167            };
3168            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
3169                return Err(KclError::new_semantic(KclErrorDetails::new(
3170                    "line's end y coordinate must be a var".to_owned(),
3171                    vec![args.source_range],
3172                )));
3173            };
3174
3175            let solver_line_p0 =
3176                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
3177            let solver_line_p1 =
3178                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
3179
3180            Ok(ConstrainableLine {
3181                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
3182                #[cfg(feature = "artifact-graph")]
3183                object_id: unsolved.object_id,
3184            })
3185        })
3186        .collect::<Result<_, _>>()?;
3187
3188    #[cfg(feature = "artifact-graph")]
3189    let constraint_id = exec_state.next_object_id();
3190    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3191        return Err(KclError::new_semantic(KclErrorDetails::new(
3192            "parallel() can only be used inside a sketch block".to_owned(),
3193            vec![args.source_range],
3194        )));
3195    };
3196
3197    let n = constrainable_lines.len();
3198    let mut constrainable_lines_iter = constrainable_lines.iter();
3199    let first_line = constrainable_lines_iter
3200        .next()
3201        .ok_or(KclError::new_semantic(KclErrorDetails::new(
3202            format!("parallel() requires at least 2 lines, but you provided {}", n),
3203            vec![args.source_range],
3204        )))?;
3205    for line in constrainable_lines_iter {
3206        sketch_state.solver_constraints.push(SolverConstraint::LinesAtAngle(
3207            first_line.solver_line,
3208            line.solver_line,
3209            AngleKind::Parallel,
3210        ));
3211    }
3212    #[cfg(feature = "artifact-graph")]
3213    {
3214        let constraint = Constraint::Parallel(Parallel {
3215            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
3216        });
3217        sketch_state.sketch_constraints.push(constraint_id);
3218        track_constraint(constraint_id, constraint, exec_state, &args);
3219    }
3220    Ok(KclValue::none())
3221}
3222
3223pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3224    lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
3225}
3226
3227/// A way to constrain points, or a line.
3228#[derive(Debug, Clone, Copy)]
3229enum AxisConstraintKind {
3230    Horizontal,
3231    Vertical,
3232}
3233
3234impl AxisConstraintKind {
3235    /// Which KCL function this corresponds to.
3236    fn function_name(self) -> &'static str {
3237        match self {
3238            AxisConstraintKind::Horizontal => "horizontal",
3239            AxisConstraintKind::Vertical => "vertical",
3240        }
3241    }
3242
3243    /// Use this constraint to align a line.
3244    fn line_constraint(self, line: DatumLineSegment) -> SolverConstraint {
3245        match self {
3246            AxisConstraintKind::Horizontal => SolverConstraint::Horizontal(line),
3247            AxisConstraintKind::Vertical => SolverConstraint::Vertical(line),
3248        }
3249    }
3250
3251    /// Use this constraint to align a pair of points.
3252    fn point_pair_constraint(self, p0: DatumPoint, p1: DatumPoint) -> SolverConstraint {
3253        match self {
3254            // A horizontal point set means all Y values are equal.
3255            AxisConstraintKind::Horizontal => SolverConstraint::VerticalDistance(p1, p0, 0.0),
3256            // A vertical point set means all X values are equal.
3257            AxisConstraintKind::Vertical => SolverConstraint::HorizontalDistance(p1, p0, 0.0),
3258        }
3259    }
3260
3261    /// Use this constraint to align a point to some known X or Y.
3262    fn constraint_aligning_point_to_constant(self, p0: DatumPoint, fixed_point: (f64, f64)) -> SolverConstraint {
3263        match self {
3264            AxisConstraintKind::Horizontal => SolverConstraint::Fixed(p0.y_id, fixed_point.1),
3265            AxisConstraintKind::Vertical => SolverConstraint::Fixed(p0.x_id, fixed_point.0),
3266        }
3267    }
3268
3269    #[cfg(feature = "artifact-graph")]
3270    fn line_artifact_constraint(self, line: ObjectId) -> Constraint {
3271        match self {
3272            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Line { line }),
3273            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Line { line }),
3274        }
3275    }
3276
3277    #[cfg(feature = "artifact-graph")]
3278    fn point_artifact_constraint(self, points: Vec<ConstraintSegment>) -> Constraint {
3279        match self {
3280            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Points { points }),
3281            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Points { points }),
3282        }
3283    }
3284}
3285
3286/// The line the user wants to align vertically/horizontally.
3287/// Extracted from KCL arguments.
3288#[derive(Debug, Clone, Copy)]
3289struct AxisLineVars {
3290    start: [SketchVarId; 2],
3291    end: [SketchVarId; 2],
3292    object_id: ObjectId,
3293}
3294
3295fn extract_axis_line_vars(
3296    segment: &AbstractSegment,
3297    kind: AxisConstraintKind,
3298    source_range: crate::SourceRange,
3299) -> Result<AxisLineVars, KclError> {
3300    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3301        return Err(KclError::new_internal(KclErrorDetails::new(
3302            "line must be an unsolved Segment".to_owned(),
3303            vec![source_range],
3304        )));
3305    };
3306    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3307        return Err(KclError::new_semantic(KclErrorDetails::new(
3308            format!(
3309                "{}() line argument must be a line, no other type of Segment",
3310                kind.function_name()
3311            ),
3312            vec![source_range],
3313        )));
3314    };
3315    let (
3316        UnsolvedExpr::Unknown(start_x),
3317        UnsolvedExpr::Unknown(start_y),
3318        UnsolvedExpr::Unknown(end_x),
3319        UnsolvedExpr::Unknown(end_y),
3320    ) = (&start[0], &start[1], &end[0], &end[1])
3321    else {
3322        return Err(KclError::new_semantic(KclErrorDetails::new(
3323            "line's x and y coordinates of both start and end must be vars".to_owned(),
3324            vec![source_range],
3325        )));
3326    };
3327
3328    Ok(AxisLineVars {
3329        start: [*start_x, *start_y],
3330        end: [*end_x, *end_y],
3331        object_id: unsolved.object_id,
3332    })
3333}
3334
3335#[derive(Debug, Clone)]
3336enum PointToAlign {
3337    /// Variable point that could be constrained.
3338    Variable { x: SketchVarId, y: SketchVarId },
3339    /// Fixed millimeter constant.
3340    Fixed { x: TyF64, y: TyF64 },
3341}
3342
3343impl From<[SketchVarId; 2]> for PointToAlign {
3344    fn from(sketch_var: [SketchVarId; 2]) -> Self {
3345        Self::Variable {
3346            x: sketch_var[0],
3347            y: sketch_var[1],
3348        }
3349    }
3350}
3351
3352impl From<[TyF64; 2]> for PointToAlign {
3353    fn from([x, y]: [TyF64; 2]) -> Self {
3354        Self::Fixed { x, y }
3355    }
3356}
3357
3358fn extract_axis_point_vars(
3359    input: &KclValue,
3360    kind: AxisConstraintKind,
3361    source_range: crate::SourceRange,
3362) -> Result<PointToAlign, KclError> {
3363    match input {
3364        KclValue::Segment { value: segment } => {
3365            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3366                return Err(KclError::new_semantic(KclErrorDetails::new(
3367                    format!(
3368                        "The `{}` function point arguments must be unsolved points",
3369                        kind.function_name()
3370                    ),
3371                    vec![source_range],
3372                )));
3373            };
3374            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
3375                return Err(KclError::new_semantic(KclErrorDetails::new(
3376                    format!(
3377                        "The `{}` function list arguments must be points, but one item is {}",
3378                        kind.function_name(),
3379                        unsolved.kind.human_friendly_kind_with_article()
3380                    ),
3381                    vec![source_range],
3382                )));
3383            };
3384            match (&position[0], &position[1]) {
3385                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed {
3386                    x: x.to_owned(),
3387                    y: y.to_owned(),
3388                }),
3389                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x: *x, y: *y }),
3390                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
3391                    Err(KclError::new_semantic(KclErrorDetails::new(
3392                        format!(
3393                            "The `{}` function cannot take a fixed X component and a variable Y component",
3394                            kind.function_name()
3395                        ),
3396                        vec![source_range],
3397                    )))
3398                }
3399                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
3400                    Err(KclError::new_semantic(KclErrorDetails::new(
3401                        format!(
3402                            "The `{}` function cannot take a fixed X component and a variable Y component",
3403                            kind.function_name()
3404                        ),
3405                        vec![source_range],
3406                    )))
3407                }
3408            }
3409        }
3410        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
3411            let [x_value, y_value] = value.as_slice() else {
3412                return Err(KclError::new_semantic(KclErrorDetails::new(
3413                    format!(
3414                        "The `{}` function point arguments must each be a Point2d like [var 0mm, var 0mm]",
3415                        kind.function_name()
3416                    ),
3417                    vec![source_range],
3418                )));
3419            };
3420            let Some(x_expr) = x_value.as_unsolved_expr() else {
3421                return Err(KclError::new_semantic(KclErrorDetails::new(
3422                    format!(
3423                        "The `{}` function point x coordinate must be a number or sketch var",
3424                        kind.function_name()
3425                    ),
3426                    vec![source_range],
3427                )));
3428            };
3429            let Some(y_expr) = y_value.as_unsolved_expr() else {
3430                return Err(KclError::new_semantic(KclErrorDetails::new(
3431                    format!(
3432                        "The `{}` function point y coordinate must be a number or sketch var",
3433                        kind.function_name()
3434                    ),
3435                    vec![source_range],
3436                )));
3437            };
3438            match (x_expr, y_expr) {
3439                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed { x, y }),
3440                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x, y }),
3441                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
3442                    Err(KclError::new_semantic(KclErrorDetails::new(
3443                        format!(
3444                            "The `{}` function cannot take a fixed X component and a variable Y component",
3445                            kind.function_name()
3446                        ),
3447                        vec![source_range],
3448                    )))
3449                }
3450                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
3451                    Err(KclError::new_semantic(KclErrorDetails::new(
3452                        format!(
3453                            "The `{}` function cannot take a fixed X component and a variable Y component",
3454                            kind.function_name()
3455                        ),
3456                        vec![source_range],
3457                    )))
3458                }
3459            }
3460        }
3461        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3462            format!(
3463                "The `{}` function accepts either a line Segment or a list of points",
3464                kind.function_name()
3465            ),
3466            vec![source_range],
3467        ))),
3468    }
3469}
3470
3471async fn axis_constraint(
3472    kind: AxisConstraintKind,
3473    exec_state: &mut ExecState,
3474    args: Args,
3475) -> Result<KclValue, KclError> {
3476    let input: KclValue =
3477        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
3478
3479    // User could pass in a single line, or a sequence of points.
3480    match input {
3481        KclValue::Segment { value } => {
3482            // Single-line case.
3483            axis_constraint_line(value, kind, exec_state, args)
3484        }
3485        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
3486            // Sequence of points case.
3487            axis_constraint_points(value, kind, exec_state, args)
3488        }
3489        other => Err(KclError::new_semantic(KclErrorDetails::new(
3490            format!(
3491                "{}() accepts either a line Segment or a list of at least two points, but you provided {}",
3492                kind.function_name(),
3493                other.human_friendly_type(),
3494            ),
3495            vec![args.source_range],
3496        ))),
3497    }
3498}
3499
3500/// User has provided a single line to align along the given axis.
3501fn axis_constraint_line(
3502    segment: Box<AbstractSegment>,
3503    kind: AxisConstraintKind,
3504    exec_state: &mut ExecState,
3505    args: Args,
3506) -> Result<KclValue, KclError> {
3507    let line = extract_axis_line_vars(&segment, kind, args.source_range)?;
3508    let range = args.source_range;
3509    let solver_p0 = DatumPoint::new_xy(
3510        line.start[0].to_constraint_id(range)?,
3511        line.start[1].to_constraint_id(range)?,
3512    );
3513    let solver_p1 = DatumPoint::new_xy(
3514        line.end[0].to_constraint_id(range)?,
3515        line.end[1].to_constraint_id(range)?,
3516    );
3517    let solver_line = DatumLineSegment::new(solver_p0, solver_p1);
3518    let constraint = kind.line_constraint(solver_line);
3519    #[cfg(feature = "artifact-graph")]
3520    let constraint_id = exec_state.next_object_id();
3521    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3522        return Err(KclError::new_semantic(KclErrorDetails::new(
3523            format!("{}() can only be used inside a sketch block", kind.function_name()),
3524            vec![args.source_range],
3525        )));
3526    };
3527    sketch_state.solver_constraints.push(constraint);
3528    #[cfg(feature = "artifact-graph")]
3529    {
3530        let constraint = kind.line_artifact_constraint(line.object_id);
3531        sketch_state.sketch_constraints.push(constraint_id);
3532        track_constraint(constraint_id, constraint, exec_state, &args);
3533    }
3534    Ok(KclValue::none())
3535}
3536
3537/// User has provided a sequence of points to align along the given axis.
3538fn axis_constraint_points(
3539    point_values: Vec<KclValue>,
3540    kind: AxisConstraintKind,
3541    exec_state: &mut ExecState,
3542    args: Args,
3543) -> Result<KclValue, KclError> {
3544    if point_values.len() < 2 {
3545        return Err(KclError::new_semantic(KclErrorDetails::new(
3546            format!("{}() point list must contain at least two points", kind.function_name()),
3547            vec![args.source_range],
3548        )));
3549    }
3550
3551    #[cfg(feature = "artifact-graph")]
3552    let trackable_point_ids = point_values
3553        .iter()
3554        .map(|point| match point {
3555            KclValue::Segment { value: segment } => {
3556                let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3557                    return None;
3558                };
3559                let UnsolvedSegmentKind::Point { .. } = &unsolved.kind else {
3560                    return None;
3561                };
3562                Some(ConstraintSegment::from(unsolved.object_id))
3563            }
3564            point if point2d_is_origin(point) => Some(ConstraintSegment::ORIGIN),
3565            _ => None,
3566        })
3567        .collect::<Option<Vec<_>>>();
3568
3569    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3570        return Err(KclError::new_semantic(KclErrorDetails::new(
3571            format!("{}() can only be used inside a sketch block", kind.function_name()),
3572            vec![args.source_range],
3573        )));
3574    };
3575
3576    let points: Vec<PointToAlign> = point_values
3577        .iter()
3578        .map(|point| extract_axis_point_vars(point, kind, args.source_range))
3579        .collect::<Result<_, _>>()?;
3580
3581    let mut solver_constraints = Vec::with_capacity(points.len().saturating_sub(1));
3582
3583    let mut var_points = Vec::new();
3584    let mut fix_points = Vec::new();
3585    for point in points {
3586        match point {
3587            PointToAlign::Variable { x, y } => var_points.push((x, y)),
3588            PointToAlign::Fixed { x, y } => fix_points.push((x, y)),
3589        }
3590    }
3591    if fix_points.len() > 1 {
3592        return Err(KclError::new_semantic(KclErrorDetails::new(
3593            format!(
3594                "{}() point list can contain at most 1 fixed point, but you provided {}",
3595                kind.function_name(),
3596                fix_points.len()
3597            ),
3598            vec![args.source_range],
3599        )));
3600    }
3601
3602    if let Some(fix_point) = fix_points.pop() {
3603        // We have to align all the variable points with this singular fixed point.
3604        // For points 0, 1, 2, ..., n, create constraints
3605        // fixed(0.x, fix.x)
3606        // fixed(1.x, fix.x)
3607        // ...
3608        // fixed(n.x, fix.x)
3609        // (or y, whatever is appropriate)
3610        for point in var_points {
3611            let solver_point = datum_point([point.0, point.1], args.source_range)?;
3612            let fix_point_mm = (fix_point.0.to_mm(), fix_point.1.to_mm());
3613            solver_constraints.push(kind.constraint_aligning_point_to_constant(solver_point, fix_point_mm));
3614        }
3615    } else {
3616        // For points 0, 1, 2, ..., n, create constraints
3617        // vertical(0, 1)
3618        // vertical(0, 2)
3619        // ...
3620        // vertical(0, n)
3621        // (or horizontal, if appropriate)
3622        let mut points = var_points.into_iter();
3623        let first_point = points.next().ok_or_else(|| {
3624            KclError::new_semantic(KclErrorDetails::new(
3625                format!("{}() point list must contain at least two points", kind.function_name()),
3626                vec![args.source_range],
3627            ))
3628        })?;
3629        let anchor = datum_point([first_point.0, first_point.1], args.source_range)?;
3630        for point in points {
3631            let solver_point = datum_point([point.0, point.1], args.source_range)?;
3632            solver_constraints.push(kind.point_pair_constraint(anchor, solver_point));
3633        }
3634    }
3635    sketch_state.solver_constraints.extend(solver_constraints);
3636
3637    #[cfg(feature = "artifact-graph")]
3638    if let Some(point_ids) = trackable_point_ids {
3639        let constraint_id = exec_state.next_object_id();
3640        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3641            debug_assert!(false, "Constraint created outside a sketch block");
3642            return Ok(KclValue::none());
3643        };
3644        sketch_state.sketch_constraints.push(constraint_id);
3645        let constraint = kind.point_artifact_constraint(point_ids);
3646        track_constraint(constraint_id, constraint, exec_state, &args);
3647    }
3648
3649    Ok(KclValue::none())
3650}
3651
3652pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3653    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3654        "lines",
3655        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3656        exec_state,
3657    )?;
3658    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
3659        KclError::new_semantic(KclErrorDetails::new(
3660            "must have two input lines".to_owned(),
3661            vec![args.source_range],
3662        ))
3663    })?;
3664    let KclValue::Segment { value: segment0 } = &line0 else {
3665        return Err(KclError::new_semantic(KclErrorDetails::new(
3666            "line argument must be a Segment".to_owned(),
3667            vec![args.source_range],
3668        )));
3669    };
3670    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
3671        return Err(KclError::new_internal(KclErrorDetails::new(
3672            "line must be an unsolved Segment".to_owned(),
3673            vec![args.source_range],
3674        )));
3675    };
3676    let UnsolvedSegmentKind::Line {
3677        start: start0,
3678        end: end0,
3679        ..
3680    } = &unsolved0.kind
3681    else {
3682        return Err(KclError::new_semantic(KclErrorDetails::new(
3683            "line argument must be a line, no other type of Segment".to_owned(),
3684            vec![args.source_range],
3685        )));
3686    };
3687    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
3688        return Err(KclError::new_semantic(KclErrorDetails::new(
3689            "line's start x coordinate must be a var".to_owned(),
3690            vec![args.source_range],
3691        )));
3692    };
3693    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
3694        return Err(KclError::new_semantic(KclErrorDetails::new(
3695            "line's start y coordinate must be a var".to_owned(),
3696            vec![args.source_range],
3697        )));
3698    };
3699    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
3700        return Err(KclError::new_semantic(KclErrorDetails::new(
3701            "line's end x coordinate must be a var".to_owned(),
3702            vec![args.source_range],
3703        )));
3704    };
3705    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
3706        return Err(KclError::new_semantic(KclErrorDetails::new(
3707            "line's end y coordinate must be a var".to_owned(),
3708            vec![args.source_range],
3709        )));
3710    };
3711    let KclValue::Segment { value: segment1 } = &line1 else {
3712        return Err(KclError::new_semantic(KclErrorDetails::new(
3713            "line argument must be a Segment".to_owned(),
3714            vec![args.source_range],
3715        )));
3716    };
3717    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
3718        return Err(KclError::new_internal(KclErrorDetails::new(
3719            "line must be an unsolved Segment".to_owned(),
3720            vec![args.source_range],
3721        )));
3722    };
3723    let UnsolvedSegmentKind::Line {
3724        start: start1,
3725        end: end1,
3726        ..
3727    } = &unsolved1.kind
3728    else {
3729        return Err(KclError::new_semantic(KclErrorDetails::new(
3730            "line argument must be a line, no other type of Segment".to_owned(),
3731            vec![args.source_range],
3732        )));
3733    };
3734    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
3735        return Err(KclError::new_semantic(KclErrorDetails::new(
3736            "line's start x coordinate must be a var".to_owned(),
3737            vec![args.source_range],
3738        )));
3739    };
3740    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
3741        return Err(KclError::new_semantic(KclErrorDetails::new(
3742            "line's start y coordinate must be a var".to_owned(),
3743            vec![args.source_range],
3744        )));
3745    };
3746    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
3747        return Err(KclError::new_semantic(KclErrorDetails::new(
3748            "line's end x coordinate must be a var".to_owned(),
3749            vec![args.source_range],
3750        )));
3751    };
3752    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3753        return Err(KclError::new_semantic(KclErrorDetails::new(
3754            "line's end y coordinate must be a var".to_owned(),
3755            vec![args.source_range],
3756        )));
3757    };
3758
3759    // All coordinates are sketch vars. Proceed.
3760    let sketch_constraint = SketchConstraint {
3761        kind: SketchConstraintKind::Angle {
3762            line0: crate::execution::ConstrainableLine2d {
3763                object_id: unsolved0.object_id,
3764                vars: [
3765                    crate::front::Point2d {
3766                        x: *line0_p0_x,
3767                        y: *line0_p0_y,
3768                    },
3769                    crate::front::Point2d {
3770                        x: *line0_p1_x,
3771                        y: *line0_p1_y,
3772                    },
3773                ],
3774            },
3775            line1: crate::execution::ConstrainableLine2d {
3776                object_id: unsolved1.object_id,
3777                vars: [
3778                    crate::front::Point2d {
3779                        x: *line1_p0_x,
3780                        y: *line1_p0_y,
3781                    },
3782                    crate::front::Point2d {
3783                        x: *line1_p1_x,
3784                        y: *line1_p1_y,
3785                    },
3786                ],
3787            },
3788        },
3789        meta: vec![args.source_range.into()],
3790    };
3791    Ok(KclValue::SketchConstraint {
3792        value: Box::new(sketch_constraint),
3793    })
3794}
3795
3796async fn lines_at_angle(
3797    angle_kind: LinesAtAngleKind,
3798    exec_state: &mut ExecState,
3799    args: Args,
3800) -> Result<KclValue, KclError> {
3801    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3802        "lines",
3803        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3804        exec_state,
3805    )?;
3806    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
3807        KclError::new_semantic(KclErrorDetails::new(
3808            "must have two input lines".to_owned(),
3809            vec![args.source_range],
3810        ))
3811    })?;
3812
3813    let KclValue::Segment { value: segment0 } = &line0 else {
3814        return Err(KclError::new_semantic(KclErrorDetails::new(
3815            "line argument must be a Segment".to_owned(),
3816            vec![args.source_range],
3817        )));
3818    };
3819    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
3820        return Err(KclError::new_internal(KclErrorDetails::new(
3821            "line must be an unsolved Segment".to_owned(),
3822            vec![args.source_range],
3823        )));
3824    };
3825    let UnsolvedSegmentKind::Line {
3826        start: start0,
3827        end: end0,
3828        ..
3829    } = &unsolved0.kind
3830    else {
3831        return Err(KclError::new_semantic(KclErrorDetails::new(
3832            "line argument must be a line, no other type of Segment".to_owned(),
3833            vec![args.source_range],
3834        )));
3835    };
3836    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
3837        return Err(KclError::new_semantic(KclErrorDetails::new(
3838            "line's start x coordinate must be a var".to_owned(),
3839            vec![args.source_range],
3840        )));
3841    };
3842    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
3843        return Err(KclError::new_semantic(KclErrorDetails::new(
3844            "line's start y coordinate must be a var".to_owned(),
3845            vec![args.source_range],
3846        )));
3847    };
3848    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
3849        return Err(KclError::new_semantic(KclErrorDetails::new(
3850            "line's end x coordinate must be a var".to_owned(),
3851            vec![args.source_range],
3852        )));
3853    };
3854    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
3855        return Err(KclError::new_semantic(KclErrorDetails::new(
3856            "line's end y coordinate must be a var".to_owned(),
3857            vec![args.source_range],
3858        )));
3859    };
3860    let KclValue::Segment { value: segment1 } = &line1 else {
3861        return Err(KclError::new_semantic(KclErrorDetails::new(
3862            "line argument must be a Segment".to_owned(),
3863            vec![args.source_range],
3864        )));
3865    };
3866    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
3867        return Err(KclError::new_internal(KclErrorDetails::new(
3868            "line must be an unsolved Segment".to_owned(),
3869            vec![args.source_range],
3870        )));
3871    };
3872    let UnsolvedSegmentKind::Line {
3873        start: start1,
3874        end: end1,
3875        ..
3876    } = &unsolved1.kind
3877    else {
3878        return Err(KclError::new_semantic(KclErrorDetails::new(
3879            "line argument must be a line, no other type of Segment".to_owned(),
3880            vec![args.source_range],
3881        )));
3882    };
3883    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
3884        return Err(KclError::new_semantic(KclErrorDetails::new(
3885            "line's start x coordinate must be a var".to_owned(),
3886            vec![args.source_range],
3887        )));
3888    };
3889    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
3890        return Err(KclError::new_semantic(KclErrorDetails::new(
3891            "line's start y coordinate must be a var".to_owned(),
3892            vec![args.source_range],
3893        )));
3894    };
3895    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
3896        return Err(KclError::new_semantic(KclErrorDetails::new(
3897            "line's end x coordinate must be a var".to_owned(),
3898            vec![args.source_range],
3899        )));
3900    };
3901    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3902        return Err(KclError::new_semantic(KclErrorDetails::new(
3903            "line's end y coordinate must be a var".to_owned(),
3904            vec![args.source_range],
3905        )));
3906    };
3907
3908    let range = args.source_range;
3909    let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3910        line0_p0_x.to_constraint_id(range)?,
3911        line0_p0_y.to_constraint_id(range)?,
3912    );
3913    let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3914        line0_p1_x.to_constraint_id(range)?,
3915        line0_p1_y.to_constraint_id(range)?,
3916    );
3917    let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
3918    let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3919        line1_p0_x.to_constraint_id(range)?,
3920        line1_p0_y.to_constraint_id(range)?,
3921    );
3922    let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3923        line1_p1_x.to_constraint_id(range)?,
3924        line1_p1_y.to_constraint_id(range)?,
3925    );
3926    let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
3927    let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
3928    #[cfg(feature = "artifact-graph")]
3929    let constraint_id = exec_state.next_object_id();
3930    // Save the constraint to be used for solving.
3931    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3932        return Err(KclError::new_semantic(KclErrorDetails::new(
3933            format!(
3934                "{}() can only be used inside a sketch block",
3935                angle_kind.to_function_name()
3936            ),
3937            vec![args.source_range],
3938        )));
3939    };
3940    sketch_state.solver_constraints.push(constraint);
3941    #[cfg(feature = "artifact-graph")]
3942    {
3943        let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
3944        sketch_state.sketch_constraints.push(constraint_id);
3945        track_constraint(constraint_id, constraint, exec_state, &args);
3946    }
3947    Ok(KclValue::none())
3948}
3949
3950pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3951    axis_constraint(AxisConstraintKind::Horizontal, exec_state, args).await
3952}
3953
3954pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3955    axis_constraint(AxisConstraintKind::Vertical, exec_state, args).await
3956}