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::Known(2),
889        ),
890        exec_state,
891    )?;
892    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
893        KclError::new_semantic(KclErrorDetails::new(
894            "must have two input points".to_owned(),
895            vec![args.source_range],
896        ))
897    })?;
898
899    let range = args.source_range;
900    match (&point0, &point1) {
901        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
902            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
903                return Err(KclError::new_semantic(KclErrorDetails::new(
904                    "first point must be an unsolved segment".to_owned(),
905                    vec![args.source_range],
906                )));
907            };
908            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
909                return Err(KclError::new_semantic(KclErrorDetails::new(
910                    "second point must be an unsolved segment".to_owned(),
911                    vec![args.source_range],
912                )));
913            };
914            match (&unsolved0.kind, &unsolved1.kind) {
915                (
916                    UnsolvedSegmentKind::Point { position: pos0, .. },
917                    UnsolvedSegmentKind::Point { position: pos1, .. },
918                ) => {
919                    let p0_x = &pos0[0];
920                    let p0_y = &pos0[1];
921                    match (p0_x, p0_y) {
922                        (UnsolvedExpr::Unknown(p0_x), UnsolvedExpr::Unknown(p0_y)) => {
923                            let p1_x = &pos1[0];
924                            let p1_y = &pos1[1];
925                            match (p1_x, p1_y) {
926                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
927                                    let constraint = SolverConstraint::PointsCoincident(
928                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
929                                            p0_x.to_constraint_id(range)?,
930                                            p0_y.to_constraint_id(range)?,
931                                        ),
932                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
933                                            p1_x.to_constraint_id(range)?,
934                                            p1_y.to_constraint_id(range)?,
935                                        ),
936                                    );
937                                    #[cfg(feature = "artifact-graph")]
938                                    let constraint_id = exec_state.next_object_id();
939                                    // Save the constraint to be used for solving.
940                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
941                                        return Err(KclError::new_semantic(KclErrorDetails::new(
942                                            "coincident() can only be used inside a sketch block".to_owned(),
943                                            vec![args.source_range],
944                                        )));
945                                    };
946                                    sketch_state.solver_constraints.push(constraint);
947                                    #[cfg(feature = "artifact-graph")]
948                                    {
949                                        let constraint = crate::front::Constraint::Coincident(Coincident {
950                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
951                                        });
952                                        sketch_state.sketch_constraints.push(constraint_id);
953                                        track_constraint(constraint_id, constraint, exec_state, &args);
954                                    }
955                                    Ok(KclValue::none())
956                                }
957                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
958                                    let p1_x = KclValue::Number {
959                                        value: p1_x.n,
960                                        ty: p1_x.ty,
961                                        meta: vec![args.source_range.into()],
962                                    };
963                                    let p1_y = KclValue::Number {
964                                        value: p1_y.n,
965                                        ty: p1_y.ty,
966                                        meta: vec![args.source_range.into()],
967                                    };
968                                    let (constraint_x, constraint_y) =
969                                        coincident_constraints_fixed(*p0_x, *p0_y, &p1_x, &p1_y, exec_state, &args)?;
970
971                                    #[cfg(feature = "artifact-graph")]
972                                    let constraint_id = exec_state.next_object_id();
973                                    // Save the constraint to be used for solving.
974                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
975                                        return Err(KclError::new_semantic(KclErrorDetails::new(
976                                            "coincident() can only be used inside a sketch block".to_owned(),
977                                            vec![args.source_range],
978                                        )));
979                                    };
980                                    sketch_state.solver_constraints.push(constraint_x);
981                                    sketch_state.solver_constraints.push(constraint_y);
982                                    #[cfg(feature = "artifact-graph")]
983                                    {
984                                        let constraint = crate::front::Constraint::Coincident(Coincident {
985                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
986                                        });
987                                        sketch_state.sketch_constraints.push(constraint_id);
988                                        track_constraint(constraint_id, constraint, exec_state, &args);
989                                    }
990                                    Ok(KclValue::none())
991                                }
992                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
993                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
994                                    // TODO: sketch-api: unimplemented
995                                    Err(KclError::new_semantic(KclErrorDetails::new(
996                                        "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(),
997                                        vec![args.source_range],
998                                    )))
999                                }
1000                            }
1001                        }
1002                        (UnsolvedExpr::Known(p0_x), UnsolvedExpr::Known(p0_y)) => {
1003                            let p1_x = &pos1[0];
1004                            let p1_y = &pos1[1];
1005                            match (p1_x, p1_y) {
1006                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1007                                    let p0_x = KclValue::Number {
1008                                        value: p0_x.n,
1009                                        ty: p0_x.ty,
1010                                        meta: vec![args.source_range.into()],
1011                                    };
1012                                    let p0_y = KclValue::Number {
1013                                        value: p0_y.n,
1014                                        ty: p0_y.ty,
1015                                        meta: vec![args.source_range.into()],
1016                                    };
1017                                    let (constraint_x, constraint_y) =
1018                                        coincident_constraints_fixed(*p1_x, *p1_y, &p0_x, &p0_y, exec_state, &args)?;
1019
1020                                    #[cfg(feature = "artifact-graph")]
1021                                    let constraint_id = exec_state.next_object_id();
1022                                    // Save the constraint to be used for solving.
1023                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1024                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1025                                            "coincident() can only be used inside a sketch block".to_owned(),
1026                                            vec![args.source_range],
1027                                        )));
1028                                    };
1029                                    sketch_state.solver_constraints.push(constraint_x);
1030                                    sketch_state.solver_constraints.push(constraint_y);
1031                                    #[cfg(feature = "artifact-graph")]
1032                                    {
1033                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1034                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1035                                        });
1036                                        sketch_state.sketch_constraints.push(constraint_id);
1037                                        track_constraint(constraint_id, constraint, exec_state, &args);
1038                                    }
1039                                    Ok(KclValue::none())
1040                                }
1041                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1042                                    if *p0_x != *p1_x || *p0_y != *p1_y {
1043                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1044                                            "Coincident constraint between two fixed points failed since coordinates differ"
1045                                                .to_owned(),
1046                                            vec![args.source_range],
1047                                        )));
1048                                    }
1049                                    Ok(KclValue::none())
1050                                }
1051                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1052                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1053                                    // TODO: sketch-api: unimplemented
1054                                    Err(KclError::new_semantic(KclErrorDetails::new(
1055                                        "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(),
1056                                        vec![args.source_range],
1057                                    )))
1058                                }
1059                            }
1060                        }
1061                        (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1062                        | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1063                            // The segment is a point with one sketch var.
1064                            Err(KclError::new_semantic(KclErrorDetails::new(
1065                                "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(),
1066                                vec![args.source_range],
1067                            )))
1068                        }
1069                    }
1070                }
1071                // Point-Line or Line-Point case: create perpendicular distance constraint with distance 0
1072                (
1073                    UnsolvedSegmentKind::Point {
1074                        position: point_pos, ..
1075                    },
1076                    UnsolvedSegmentKind::Line {
1077                        start: line_start,
1078                        end: line_end,
1079                        ..
1080                    },
1081                )
1082                | (
1083                    UnsolvedSegmentKind::Line {
1084                        start: line_start,
1085                        end: line_end,
1086                        ..
1087                    },
1088                    UnsolvedSegmentKind::Point {
1089                        position: point_pos, ..
1090                    },
1091                ) => {
1092                    let point_x = &point_pos[0];
1093                    let point_y = &point_pos[1];
1094                    match (point_x, point_y) {
1095                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1096                            // Extract line start and end coordinates
1097                            let (start_x, start_y) = (&line_start[0], &line_start[1]);
1098                            let (end_x, end_y) = (&line_end[0], &line_end[1]);
1099
1100                            match (start_x, start_y, end_x, end_y) {
1101                                (
1102                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1103                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1104                                ) => {
1105                                    let point = DatumPoint::new_xy(
1106                                        point_x.to_constraint_id(range)?,
1107                                        point_y.to_constraint_id(range)?,
1108                                    );
1109                                    let line_segment = DatumLineSegment::new(
1110                                        DatumPoint::new_xy(sx.to_constraint_id(range)?, sy.to_constraint_id(range)?),
1111                                        DatumPoint::new_xy(ex.to_constraint_id(range)?, ey.to_constraint_id(range)?),
1112                                    );
1113                                    let constraint = SolverConstraint::PointLineDistance(point, line_segment, 0.0);
1114
1115                                    #[cfg(feature = "artifact-graph")]
1116                                    let constraint_id = exec_state.next_object_id();
1117
1118                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1119                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1120                                            "coincident() can only be used inside a sketch block".to_owned(),
1121                                            vec![args.source_range],
1122                                        )));
1123                                    };
1124                                    sketch_state.solver_constraints.push(constraint);
1125                                    #[cfg(feature = "artifact-graph")]
1126                                    {
1127                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1128                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1129                                        });
1130                                        sketch_state.sketch_constraints.push(constraint_id);
1131                                        track_constraint(constraint_id, constraint, exec_state, &args);
1132                                    }
1133                                    Ok(KclValue::none())
1134                                }
1135                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1136                                    "Line segment endpoints must be sketch variables for point-segment coincident constraint".to_owned(),
1137                                    vec![args.source_range],
1138                                ))),
1139                            }
1140                        }
1141                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1142                            "Point coordinates must be sketch variables for point-segment coincident constraint"
1143                                .to_owned(),
1144                            vec![args.source_range],
1145                        ))),
1146                    }
1147                }
1148                // Point-Arc or Arc-Point case: create PointArcCoincident constraint
1149                (
1150                    UnsolvedSegmentKind::Point {
1151                        position: point_pos, ..
1152                    },
1153                    UnsolvedSegmentKind::Arc {
1154                        start: arc_start,
1155                        end: arc_end,
1156                        center: arc_center,
1157                        ..
1158                    },
1159                )
1160                | (
1161                    UnsolvedSegmentKind::Arc {
1162                        start: arc_start,
1163                        end: arc_end,
1164                        center: arc_center,
1165                        ..
1166                    },
1167                    UnsolvedSegmentKind::Point {
1168                        position: point_pos, ..
1169                    },
1170                ) => {
1171                    let point_x = &point_pos[0];
1172                    let point_y = &point_pos[1];
1173                    match (point_x, point_y) {
1174                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1175                            // Extract arc center, start, and end coordinates
1176                            let (center_x, center_y) = (&arc_center[0], &arc_center[1]);
1177                            let (start_x, start_y) = (&arc_start[0], &arc_start[1]);
1178                            let (end_x, end_y) = (&arc_end[0], &arc_end[1]);
1179
1180                            match (center_x, center_y, start_x, start_y, end_x, end_y) {
1181                                (
1182                                    UnsolvedExpr::Unknown(cx), UnsolvedExpr::Unknown(cy),
1183                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1184                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1185                                ) => {
1186                                    let point = DatumPoint::new_xy(
1187                                        point_x.to_constraint_id(range)?,
1188                                        point_y.to_constraint_id(range)?,
1189                                    );
1190                                    let circular_arc = DatumCircularArc {
1191                                        center: DatumPoint::new_xy(
1192                                            cx.to_constraint_id(range)?,
1193                                            cy.to_constraint_id(range)?,
1194                                        ),
1195                                        start: DatumPoint::new_xy(
1196                                            sx.to_constraint_id(range)?,
1197                                            sy.to_constraint_id(range)?,
1198                                        ),
1199                                        end: DatumPoint::new_xy(
1200                                            ex.to_constraint_id(range)?,
1201                                            ey.to_constraint_id(range)?,
1202                                        ),
1203                                    };
1204                                    let constraint = SolverConstraint::PointArcCoincident(circular_arc, point);
1205
1206                                    #[cfg(feature = "artifact-graph")]
1207                                    let constraint_id = exec_state.next_object_id();
1208
1209                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1210                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1211                                            "coincident() can only be used inside a sketch block".to_owned(),
1212                                            vec![args.source_range],
1213                                        )));
1214                                    };
1215                                    sketch_state.solver_constraints.push(constraint);
1216                                    #[cfg(feature = "artifact-graph")]
1217                                    {
1218                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1219                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1220                                        });
1221                                        sketch_state.sketch_constraints.push(constraint_id);
1222                                        track_constraint(constraint_id, constraint, exec_state, &args);
1223                                    }
1224                                    Ok(KclValue::none())
1225                                }
1226                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1227                                    "Arc center, start, and end points must be sketch variables for point-arc coincident constraint".to_owned(),
1228                                    vec![args.source_range],
1229                                ))),
1230                            }
1231                        }
1232                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1233                            "Point coordinates must be sketch variables for point-arc coincident constraint".to_owned(),
1234                            vec![args.source_range],
1235                        ))),
1236                    }
1237                }
1238                // Point-Circle or Circle-Point case: constrain point-to-center distance
1239                // to equal the circle radius.
1240                (
1241                    UnsolvedSegmentKind::Point {
1242                        position: point_pos, ..
1243                    },
1244                    UnsolvedSegmentKind::Circle {
1245                        start: circle_start,
1246                        center: circle_center,
1247                        ..
1248                    },
1249                )
1250                | (
1251                    UnsolvedSegmentKind::Circle {
1252                        start: circle_start,
1253                        center: circle_center,
1254                        ..
1255                    },
1256                    UnsolvedSegmentKind::Point {
1257                        position: point_pos, ..
1258                    },
1259                ) => {
1260                    let point_x = &point_pos[0];
1261                    let point_y = &point_pos[1];
1262                    match (point_x, point_y) {
1263                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1264                            // Extract circle center and start coordinates.
1265                            let (center_x, center_y) = (&circle_center[0], &circle_center[1]);
1266                            let (start_x, start_y) = (&circle_start[0], &circle_start[1]);
1267
1268                            match (center_x, center_y, start_x, start_y) {
1269                                (
1270                                    UnsolvedExpr::Unknown(cx),
1271                                    UnsolvedExpr::Unknown(cy),
1272                                    UnsolvedExpr::Unknown(sx),
1273                                    UnsolvedExpr::Unknown(sy),
1274                                ) => {
1275                                    let point_radius_line = DatumLineSegment::new(
1276                                        DatumPoint::new_xy(
1277                                            cx.to_constraint_id(range)?,
1278                                            cy.to_constraint_id(range)?,
1279                                        ),
1280                                        DatumPoint::new_xy(
1281                                            point_x.to_constraint_id(range)?,
1282                                            point_y.to_constraint_id(range)?,
1283                                        ),
1284                                    );
1285                                    let circle_radius_line = DatumLineSegment::new(
1286                                        DatumPoint::new_xy(
1287                                            cx.to_constraint_id(range)?,
1288                                            cy.to_constraint_id(range)?,
1289                                        ),
1290                                        DatumPoint::new_xy(
1291                                            sx.to_constraint_id(range)?,
1292                                            sy.to_constraint_id(range)?,
1293                                        ),
1294                                    );
1295                                    let constraint =
1296                                        SolverConstraint::LinesEqualLength(point_radius_line, circle_radius_line);
1297
1298                                    #[cfg(feature = "artifact-graph")]
1299                                    let constraint_id = exec_state.next_object_id();
1300
1301                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1302                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1303                                            "coincident() can only be used inside a sketch block".to_owned(),
1304                                            vec![args.source_range],
1305                                        )));
1306                                    };
1307                                    sketch_state.solver_constraints.push(constraint);
1308                                    #[cfg(feature = "artifact-graph")]
1309                                    {
1310                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1311                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1312                                        });
1313                                        sketch_state.sketch_constraints.push(constraint_id);
1314                                        track_constraint(constraint_id, constraint, exec_state, &args);
1315                                    }
1316                                    Ok(KclValue::none())
1317                                }
1318                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1319                                    "Circle start and center points must be sketch variables for point-circle coincident constraint".to_owned(),
1320                                    vec![args.source_range],
1321                                ))),
1322                            }
1323                        }
1324                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1325                            "Point coordinates must be sketch variables for point-circle coincident constraint"
1326                                .to_owned(),
1327                            vec![args.source_range],
1328                        ))),
1329                    }
1330                }
1331                // Line-Line case: create parallel constraint and perpendicular distance of zero
1332                (
1333                    UnsolvedSegmentKind::Line {
1334                        start: line0_start,
1335                        end: line0_end,
1336                        ..
1337                    },
1338                    UnsolvedSegmentKind::Line {
1339                        start: line1_start,
1340                        end: line1_end,
1341                        ..
1342                    },
1343                ) => {
1344                    // Extract line coordinates
1345                    let (line0_start_x, line0_start_y) = (&line0_start[0], &line0_start[1]);
1346                    let (line0_end_x, line0_end_y) = (&line0_end[0], &line0_end[1]);
1347                    let (line1_start_x, line1_start_y) = (&line1_start[0], &line1_start[1]);
1348                    let (line1_end_x, line1_end_y) = (&line1_end[0], &line1_end[1]);
1349
1350                    match (
1351                        line0_start_x,
1352                        line0_start_y,
1353                        line0_end_x,
1354                        line0_end_y,
1355                        line1_start_x,
1356                        line1_start_y,
1357                        line1_end_x,
1358                        line1_end_y,
1359                    ) {
1360                        (
1361                            UnsolvedExpr::Unknown(l0_sx),
1362                            UnsolvedExpr::Unknown(l0_sy),
1363                            UnsolvedExpr::Unknown(l0_ex),
1364                            UnsolvedExpr::Unknown(l0_ey),
1365                            UnsolvedExpr::Unknown(l1_sx),
1366                            UnsolvedExpr::Unknown(l1_sy),
1367                            UnsolvedExpr::Unknown(l1_ex),
1368                            UnsolvedExpr::Unknown(l1_ey),
1369                        ) => {
1370                            // Create line segments for the solver
1371                            let line0_segment = DatumLineSegment::new(
1372                                DatumPoint::new_xy(l0_sx.to_constraint_id(range)?, l0_sy.to_constraint_id(range)?),
1373                                DatumPoint::new_xy(l0_ex.to_constraint_id(range)?, l0_ey.to_constraint_id(range)?),
1374                            );
1375                            let line1_segment = DatumLineSegment::new(
1376                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?),
1377                                DatumPoint::new_xy(l1_ex.to_constraint_id(range)?, l1_ey.to_constraint_id(range)?),
1378                            );
1379
1380                            // Create parallel constraint
1381                            let parallel_constraint =
1382                                SolverConstraint::LinesAtAngle(line0_segment, line1_segment, AngleKind::Parallel);
1383
1384                            // Create perpendicular distance constraint from first line to start point of second line
1385                            let point_on_line1 =
1386                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?);
1387                            let distance_constraint =
1388                                SolverConstraint::PointLineDistance(point_on_line1, line0_segment, 0.0);
1389
1390                            #[cfg(feature = "artifact-graph")]
1391                            let constraint_id = exec_state.next_object_id();
1392
1393                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1394                                return Err(KclError::new_semantic(KclErrorDetails::new(
1395                                    "coincident() can only be used inside a sketch block".to_owned(),
1396                                    vec![args.source_range],
1397                                )));
1398                            };
1399                            // Push both constraints to achieve collinearity
1400                            sketch_state.solver_constraints.push(parallel_constraint);
1401                            sketch_state.solver_constraints.push(distance_constraint);
1402                            #[cfg(feature = "artifact-graph")]
1403                            {
1404                                let constraint = crate::front::Constraint::Coincident(Coincident {
1405                                    segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1406                                });
1407                                sketch_state.sketch_constraints.push(constraint_id);
1408                                track_constraint(constraint_id, constraint, exec_state, &args);
1409                            }
1410                            Ok(KclValue::none())
1411                        }
1412                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1413                            "Line segment endpoints must be sketch variables for line-line coincident constraint"
1414                                .to_owned(),
1415                            vec![args.source_range],
1416                        ))),
1417                    }
1418                }
1419                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1420                    format!(
1421                        "coincident supports point-point, point-segment, or segment-segment; found {:?} and {:?}",
1422                        &unsolved0.kind, &unsolved1.kind
1423                    ),
1424                    vec![args.source_range],
1425                ))),
1426            }
1427        }
1428        // One argument is a Segment and the other is a Point2d literal.
1429        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1430        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1431            let Some(pt) = <[TyF64; 2]>::from_kcl_val(point2d) else {
1432                return Err(KclError::new_semantic(KclErrorDetails::new(
1433                    "Expected a Segment or Point2d (e.g. [1mm, 2mm])".to_owned(),
1434                    vec![args.source_range],
1435                )));
1436            };
1437            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1438                return Err(KclError::new_semantic(KclErrorDetails::new(
1439                    "segment must be an unsolved segment".to_owned(),
1440                    vec![args.source_range],
1441                )));
1442            };
1443            match &unsolved.kind {
1444                UnsolvedSegmentKind::Point { position, .. } => {
1445                    let p_x = &position[0];
1446                    let p_y = &position[1];
1447                    match (p_x, p_y) {
1448                        (UnsolvedExpr::Unknown(p_x), UnsolvedExpr::Unknown(p_y)) => {
1449                            let pt_x = KclValue::Number {
1450                                value: pt[0].n,
1451                                ty: pt[0].ty,
1452                                meta: vec![args.source_range.into()],
1453                            };
1454                            let pt_y = KclValue::Number {
1455                                value: pt[1].n,
1456                                ty: pt[1].ty,
1457                                meta: vec![args.source_range.into()],
1458                            };
1459                            let (constraint_x, constraint_y) =
1460                                coincident_constraints_fixed(*p_x, *p_y, &pt_x, &pt_y, exec_state, &args)?;
1461
1462                            #[cfg(feature = "artifact-graph")]
1463                            let constraint_id = exec_state.next_object_id();
1464                            #[cfg(feature = "artifact-graph")]
1465                            let coincident_segments = coincident_segments_for_segment_and_point2d(
1466                                unsolved.object_id,
1467                                point2d,
1468                                matches!((&point0, &point1), (KclValue::Segment { .. }, _)),
1469                            );
1470                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1471                                return Err(KclError::new_semantic(KclErrorDetails::new(
1472                                    "coincident() can only be used inside a sketch block".to_owned(),
1473                                    vec![args.source_range],
1474                                )));
1475                            };
1476                            sketch_state.solver_constraints.push(constraint_x);
1477                            sketch_state.solver_constraints.push(constraint_y);
1478                            #[cfg(feature = "artifact-graph")]
1479                            {
1480                                let constraint = crate::front::Constraint::Coincident(Coincident {
1481                                    segments: coincident_segments,
1482                                });
1483                                sketch_state.sketch_constraints.push(constraint_id);
1484                                track_constraint(constraint_id, constraint, exec_state, &args);
1485                            }
1486                            Ok(KclValue::none())
1487                        }
1488                        (UnsolvedExpr::Known(known_x), UnsolvedExpr::Known(known_y)) => {
1489                            let pt_x_val = normalize_to_solver_distance_unit(
1490                                &KclValue::Number {
1491                                    value: pt[0].n,
1492                                    ty: pt[0].ty,
1493                                    meta: vec![args.source_range.into()],
1494                                },
1495                                args.source_range,
1496                                exec_state,
1497                                "coincident constraint value",
1498                            )?;
1499                            let pt_y_val = normalize_to_solver_distance_unit(
1500                                &KclValue::Number {
1501                                    value: pt[1].n,
1502                                    ty: pt[1].ty,
1503                                    meta: vec![args.source_range.into()],
1504                                },
1505                                args.source_range,
1506                                exec_state,
1507                                "coincident constraint value",
1508                            )?;
1509                            let Some(pt_x) = pt_x_val.as_ty_f64() else {
1510                                return Err(KclError::new_semantic(KclErrorDetails::new(
1511                                    "Expected number for Point2d x coordinate".to_owned(),
1512                                    vec![args.source_range],
1513                                )));
1514                            };
1515                            let Some(pt_y) = pt_y_val.as_ty_f64() else {
1516                                return Err(KclError::new_semantic(KclErrorDetails::new(
1517                                    "Expected number for Point2d y coordinate".to_owned(),
1518                                    vec![args.source_range],
1519                                )));
1520                            };
1521                            let known_x_val = normalize_to_solver_distance_unit(
1522                                &KclValue::Number {
1523                                    value: known_x.n,
1524                                    ty: known_x.ty,
1525                                    meta: vec![args.source_range.into()],
1526                                },
1527                                args.source_range,
1528                                exec_state,
1529                                "coincident constraint value",
1530                            )?;
1531                            let Some(known_x_f) = known_x_val.as_ty_f64() else {
1532                                return Err(KclError::new_semantic(KclErrorDetails::new(
1533                                    "Expected number for known x coordinate".to_owned(),
1534                                    vec![args.source_range],
1535                                )));
1536                            };
1537                            let known_y_val = normalize_to_solver_distance_unit(
1538                                &KclValue::Number {
1539                                    value: known_y.n,
1540                                    ty: known_y.ty,
1541                                    meta: vec![args.source_range.into()],
1542                                },
1543                                args.source_range,
1544                                exec_state,
1545                                "coincident constraint value",
1546                            )?;
1547                            let Some(known_y_f) = known_y_val.as_ty_f64() else {
1548                                return Err(KclError::new_semantic(KclErrorDetails::new(
1549                                    "Expected number for known y coordinate".to_owned(),
1550                                    vec![args.source_range],
1551                                )));
1552                            };
1553                            if known_x_f.n != pt_x.n || known_y_f.n != pt_y.n {
1554                                return Err(KclError::new_semantic(KclErrorDetails::new(
1555                                    "Coincident constraint between two fixed points failed since coordinates differ"
1556                                        .to_owned(),
1557                                    vec![args.source_range],
1558                                )));
1559                            }
1560                            Ok(KclValue::none())
1561                        }
1562                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1563                            "Point coordinates must have consistent known/unknown status for coincident constraint"
1564                                .to_owned(),
1565                            vec![args.source_range],
1566                        ))),
1567                    }
1568                }
1569                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1570                    "A Point2d can only be constrained coincident with a point segment, not a line or arc".to_owned(),
1571                    vec![args.source_range],
1572                ))),
1573            }
1574        }
1575        // Both arguments are Point2d literals -- just verify equality.
1576        _ => {
1577            let pt0 = <[TyF64; 2]>::from_kcl_val(&point0);
1578            let pt1 = <[TyF64; 2]>::from_kcl_val(&point1);
1579            match (pt0, pt1) {
1580                (Some(a), Some(b)) => {
1581                    // Normalize both to solver units and compare.
1582                    let a_x = normalize_to_solver_distance_unit(
1583                        &KclValue::Number {
1584                            value: a[0].n,
1585                            ty: a[0].ty,
1586                            meta: vec![args.source_range.into()],
1587                        },
1588                        args.source_range,
1589                        exec_state,
1590                        "coincident constraint value",
1591                    )?;
1592                    let a_y = normalize_to_solver_distance_unit(
1593                        &KclValue::Number {
1594                            value: a[1].n,
1595                            ty: a[1].ty,
1596                            meta: vec![args.source_range.into()],
1597                        },
1598                        args.source_range,
1599                        exec_state,
1600                        "coincident constraint value",
1601                    )?;
1602                    let b_x = normalize_to_solver_distance_unit(
1603                        &KclValue::Number {
1604                            value: b[0].n,
1605                            ty: b[0].ty,
1606                            meta: vec![args.source_range.into()],
1607                        },
1608                        args.source_range,
1609                        exec_state,
1610                        "coincident constraint value",
1611                    )?;
1612                    let b_y = normalize_to_solver_distance_unit(
1613                        &KclValue::Number {
1614                            value: b[1].n,
1615                            ty: b[1].ty,
1616                            meta: vec![args.source_range.into()],
1617                        },
1618                        args.source_range,
1619                        exec_state,
1620                        "coincident constraint value",
1621                    )?;
1622                    if a_x.as_ty_f64().map(|v| v.n) != b_x.as_ty_f64().map(|v| v.n)
1623                        || a_y.as_ty_f64().map(|v| v.n) != b_y.as_ty_f64().map(|v| v.n)
1624                    {
1625                        return Err(KclError::new_semantic(KclErrorDetails::new(
1626                            "Coincident constraint between two fixed points failed since coordinates differ".to_owned(),
1627                            vec![args.source_range],
1628                        )));
1629                    }
1630                    Ok(KclValue::none())
1631                }
1632                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1633                    "All inputs must be Segments or Point2d values".to_owned(),
1634                    vec![args.source_range],
1635                ))),
1636            }
1637        }
1638    }
1639}
1640
1641#[cfg(feature = "artifact-graph")]
1642fn track_constraint(constraint_id: ObjectId, constraint: Constraint, exec_state: &mut ExecState, args: &Args) {
1643    let sketch_id = {
1644        let Some(sketch_state) = exec_state.sketch_block_mut() else {
1645            debug_assert!(false, "Constraint created outside a sketch block");
1646            return;
1647        };
1648        sketch_state.sketch_id
1649    };
1650    let Some(sketch_id) = sketch_id else {
1651        debug_assert!(false, "Constraint created without a sketch id");
1652        return;
1653    };
1654    let artifact_id = exec_state.next_artifact_id();
1655    exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
1656        id: artifact_id,
1657        sketch_id,
1658        constraint_id,
1659        constraint_type: SketchBlockConstraintType::from(&constraint),
1660        code_ref: CodeRef::placeholder(args.source_range),
1661    }));
1662    exec_state.add_scene_object(
1663        Object {
1664            id: constraint_id,
1665            kind: ObjectKind::Constraint { constraint },
1666            label: Default::default(),
1667            comments: Default::default(),
1668            artifact_id,
1669            source: SourceRef::new(args.source_range, args.node_path.clone()),
1670        },
1671        args.source_range,
1672    );
1673}
1674
1675/// Order of points has been erased when calling this function.
1676fn coincident_constraints_fixed(
1677    p0_x: SketchVarId,
1678    p0_y: SketchVarId,
1679    p1_x: &KclValue,
1680    p1_y: &KclValue,
1681    exec_state: &mut ExecState,
1682    args: &Args,
1683) -> Result<(ezpz::Constraint, ezpz::Constraint), KclError> {
1684    let p1_x_number_value =
1685        normalize_to_solver_distance_unit(p1_x, p1_x.into(), exec_state, "coincident constraint value")?;
1686    let p1_y_number_value =
1687        normalize_to_solver_distance_unit(p1_y, p1_y.into(), exec_state, "coincident constraint value")?;
1688    let Some(p1_x) = p1_x_number_value.as_ty_f64() else {
1689        let message = format!(
1690            "Expected number after coercion, but found {}",
1691            p1_x_number_value.human_friendly_type()
1692        );
1693        debug_assert!(false, "{}", &message);
1694        return Err(KclError::new_internal(KclErrorDetails::new(
1695            message,
1696            vec![args.source_range],
1697        )));
1698    };
1699    let Some(p1_y) = p1_y_number_value.as_ty_f64() else {
1700        let message = format!(
1701            "Expected number after coercion, but found {}",
1702            p1_y_number_value.human_friendly_type()
1703        );
1704        debug_assert!(false, "{}", &message);
1705        return Err(KclError::new_internal(KclErrorDetails::new(
1706            message,
1707            vec![args.source_range],
1708        )));
1709    };
1710    let constraint_x = SolverConstraint::Fixed(p0_x.to_constraint_id(args.source_range)?, p1_x.n);
1711    let constraint_y = SolverConstraint::Fixed(p0_y.to_constraint_id(args.source_range)?, p1_y.n);
1712    Ok((constraint_x, constraint_y))
1713}
1714
1715pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1716    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1717        "points",
1718        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
1719        exec_state,
1720    )?;
1721    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
1722        KclError::new_semantic(KclErrorDetails::new(
1723            "must have two input points".to_owned(),
1724            vec![args.source_range],
1725        ))
1726    })?;
1727
1728    match (&point0, &point1) {
1729        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1730            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1731                return Err(KclError::new_semantic(KclErrorDetails::new(
1732                    "first point must be an unsolved segment".to_owned(),
1733                    vec![args.source_range],
1734                )));
1735            };
1736            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1737                return Err(KclError::new_semantic(KclErrorDetails::new(
1738                    "second point must be an unsolved segment".to_owned(),
1739                    vec![args.source_range],
1740                )));
1741            };
1742            match (&unsolved0.kind, &unsolved1.kind) {
1743                (
1744                    UnsolvedSegmentKind::Point { position: pos0, .. },
1745                    UnsolvedSegmentKind::Point { position: pos1, .. },
1746                ) => {
1747                    // Both segments are points. Create a distance constraint
1748                    // between them.
1749                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
1750                        (
1751                            UnsolvedExpr::Unknown(p0_x),
1752                            UnsolvedExpr::Unknown(p0_y),
1753                            UnsolvedExpr::Unknown(p1_x),
1754                            UnsolvedExpr::Unknown(p1_y),
1755                        ) => {
1756                            // All coordinates are sketch vars. Proceed.
1757                            let sketch_constraint = SketchConstraint {
1758                                kind: SketchConstraintKind::Distance {
1759                                    points: [
1760                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1761                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
1762                                            object_id: unsolved0.object_id,
1763                                        }),
1764                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1765                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
1766                                            object_id: unsolved1.object_id,
1767                                        }),
1768                                    ],
1769                                },
1770                                meta: vec![args.source_range.into()],
1771                            };
1772                            Ok(KclValue::SketchConstraint {
1773                                value: Box::new(sketch_constraint),
1774                            })
1775                        }
1776                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1777                            "unimplemented: distance() arguments must be all sketch vars in all coordinates".to_owned(),
1778                            vec![args.source_range],
1779                        ))),
1780                    }
1781                }
1782                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1783                    "distance() arguments must be unsolved points".to_owned(),
1784                    vec![args.source_range],
1785                ))),
1786            }
1787        }
1788        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1789        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1790            if !point2d_is_origin(point2d) {
1791                return Err(KclError::new_semantic(KclErrorDetails::new(
1792                    "distance() Point2d arguments must be ORIGIN".to_owned(),
1793                    vec![args.source_range],
1794                )));
1795            }
1796
1797            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1798                return Err(KclError::new_semantic(KclErrorDetails::new(
1799                    "segment must be an unsolved segment".to_owned(),
1800                    vec![args.source_range],
1801                )));
1802            };
1803            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
1804                return Err(KclError::new_semantic(KclErrorDetails::new(
1805                    "distance() arguments must be unsolved points or ORIGIN".to_owned(),
1806                    vec![args.source_range],
1807                )));
1808            };
1809            match (&position[0], &position[1]) {
1810                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1811                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1812                        vars: crate::front::Point2d {
1813                            x: *point_x,
1814                            y: *point_y,
1815                        },
1816                        object_id: unsolved.object_id,
1817                    });
1818                    let points = if matches!((&point0, &point1), (KclValue::Segment { .. }, _)) {
1819                        [point, ConstrainablePoint2dOrOrigin::Origin]
1820                    } else {
1821                        [ConstrainablePoint2dOrOrigin::Origin, point]
1822                    };
1823                    Ok(KclValue::SketchConstraint {
1824                        value: Box::new(SketchConstraint {
1825                            kind: SketchConstraintKind::Distance { points },
1826                            meta: vec![args.source_range.into()],
1827                        }),
1828                    })
1829                }
1830                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1831                    "unimplemented: distance() point arguments must be sketch vars in all coordinates".to_owned(),
1832                    vec![args.source_range],
1833                ))),
1834            }
1835        }
1836        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1837            "distance() arguments must be point segments or ORIGIN".to_owned(),
1838            vec![args.source_range],
1839        ))),
1840    }
1841}
1842
1843/// Helper function to create a radius or diameter constraint from a circular segment.
1844/// Used by both radius() and diameter() functions.
1845fn create_circular_radius_constraint(
1846    segment: KclValue,
1847    constraint_kind: fn([ConstrainablePoint2d; 2]) -> SketchConstraintKind,
1848    source_range: crate::SourceRange,
1849) -> Result<SketchConstraint, KclError> {
1850    // Create a dummy constraint to get its name for error messages
1851    let dummy_constraint = constraint_kind([
1852        ConstrainablePoint2d {
1853            vars: crate::front::Point2d {
1854                x: SketchVarId(0),
1855                y: SketchVarId(0),
1856            },
1857            object_id: ObjectId(0),
1858        },
1859        ConstrainablePoint2d {
1860            vars: crate::front::Point2d {
1861                x: SketchVarId(0),
1862                y: SketchVarId(0),
1863            },
1864            object_id: ObjectId(0),
1865        },
1866    ]);
1867    let function_name = dummy_constraint.name();
1868
1869    let KclValue::Segment { value: seg } = segment else {
1870        return Err(KclError::new_semantic(KclErrorDetails::new(
1871            format!("{}() argument must be a segment", function_name),
1872            vec![source_range],
1873        )));
1874    };
1875    let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1876        return Err(KclError::new_semantic(KclErrorDetails::new(
1877            "segment must be unsolved".to_owned(),
1878            vec![source_range],
1879        )));
1880    };
1881    match &unsolved.kind {
1882        UnsolvedSegmentKind::Arc {
1883            center,
1884            start,
1885            center_object_id,
1886            start_object_id,
1887            ..
1888        }
1889        | UnsolvedSegmentKind::Circle {
1890            center,
1891            start,
1892            center_object_id,
1893            start_object_id,
1894            ..
1895        } => {
1896            // Extract center and start point coordinates
1897            match (&center[0], &center[1], &start[0], &start[1]) {
1898                (
1899                    UnsolvedExpr::Unknown(center_x),
1900                    UnsolvedExpr::Unknown(center_y),
1901                    UnsolvedExpr::Unknown(start_x),
1902                    UnsolvedExpr::Unknown(start_y),
1903                ) => {
1904                    // All coordinates are sketch vars. Create constraint.
1905                    let sketch_constraint = SketchConstraint {
1906                        kind: constraint_kind([
1907                            ConstrainablePoint2d {
1908                                vars: crate::front::Point2d {
1909                                    x: *center_x,
1910                                    y: *center_y,
1911                                },
1912                                object_id: *center_object_id,
1913                            },
1914                            ConstrainablePoint2d {
1915                                vars: crate::front::Point2d {
1916                                    x: *start_x,
1917                                    y: *start_y,
1918                                },
1919                                object_id: *start_object_id,
1920                            },
1921                        ]),
1922                        meta: vec![source_range.into()],
1923                    };
1924                    Ok(sketch_constraint)
1925                }
1926                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1927                    format!(
1928                        "unimplemented: {}() arc or circle segment must have all sketch vars in all coordinates",
1929                        function_name
1930                    ),
1931                    vec![source_range],
1932                ))),
1933            }
1934        }
1935        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1936            format!("{}() argument must be an arc or circle segment", function_name),
1937            vec![source_range],
1938        ))),
1939    }
1940}
1941
1942pub async fn radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1943    let segment: KclValue =
1944        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
1945
1946    create_circular_radius_constraint(
1947        segment,
1948        |points| SketchConstraintKind::Radius { points },
1949        args.source_range,
1950    )
1951    .map(|constraint| KclValue::SketchConstraint {
1952        value: Box::new(constraint),
1953    })
1954}
1955
1956pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1957    let segment: KclValue =
1958        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
1959
1960    create_circular_radius_constraint(
1961        segment,
1962        |points| SketchConstraintKind::Diameter { points },
1963        args.source_range,
1964    )
1965    .map(|constraint| KclValue::SketchConstraint {
1966        value: Box::new(constraint),
1967    })
1968}
1969
1970pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1971    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1972        "points",
1973        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
1974        exec_state,
1975    )?;
1976    let [p1, p2] = points.as_slice() else {
1977        return Err(KclError::new_semantic(KclErrorDetails::new(
1978            "must have two input points".to_owned(),
1979            vec![args.source_range],
1980        )));
1981    };
1982    match (p1, p2) {
1983        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1984            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1985                return Err(KclError::new_semantic(KclErrorDetails::new(
1986                    "first point must be an unsolved segment".to_owned(),
1987                    vec![args.source_range],
1988                )));
1989            };
1990            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1991                return Err(KclError::new_semantic(KclErrorDetails::new(
1992                    "second point must be an unsolved segment".to_owned(),
1993                    vec![args.source_range],
1994                )));
1995            };
1996            match (&unsolved0.kind, &unsolved1.kind) {
1997                (
1998                    UnsolvedSegmentKind::Point { position: pos0, .. },
1999                    UnsolvedSegmentKind::Point { position: pos1, .. },
2000                ) => {
2001                    // Both segments are points. Create a horizontal distance constraint
2002                    // between them.
2003                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2004                        (
2005                            UnsolvedExpr::Unknown(p0_x),
2006                            UnsolvedExpr::Unknown(p0_y),
2007                            UnsolvedExpr::Unknown(p1_x),
2008                            UnsolvedExpr::Unknown(p1_y),
2009                        ) => {
2010                            // All coordinates are sketch vars. Proceed.
2011                            let sketch_constraint = SketchConstraint {
2012                                kind: SketchConstraintKind::HorizontalDistance {
2013                                    points: [
2014                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2015                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2016                                            object_id: unsolved0.object_id,
2017                                        }),
2018                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2019                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2020                                            object_id: unsolved1.object_id,
2021                                        }),
2022                                    ],
2023                                },
2024                                meta: vec![args.source_range.into()],
2025                            };
2026                            Ok(KclValue::SketchConstraint {
2027                                value: Box::new(sketch_constraint),
2028                            })
2029                        }
2030                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2031                            "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
2032                                .to_owned(),
2033                            vec![args.source_range],
2034                        ))),
2035                    }
2036                }
2037                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2038                    "horizontalDistance() arguments must be unsolved points".to_owned(),
2039                    vec![args.source_range],
2040                ))),
2041            }
2042        }
2043        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2044        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2045            if !point2d_is_origin(point2d) {
2046                return Err(KclError::new_semantic(KclErrorDetails::new(
2047                    "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
2048                    vec![args.source_range],
2049                )));
2050            }
2051
2052            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2053                return Err(KclError::new_semantic(KclErrorDetails::new(
2054                    "segment must be an unsolved segment".to_owned(),
2055                    vec![args.source_range],
2056                )));
2057            };
2058            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2059                return Err(KclError::new_semantic(KclErrorDetails::new(
2060                    "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2061                    vec![args.source_range],
2062                )));
2063            };
2064            match (&position[0], &position[1]) {
2065                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2066                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2067                        vars: crate::front::Point2d {
2068                            x: *point_x,
2069                            y: *point_y,
2070                        },
2071                        object_id: unsolved.object_id,
2072                    });
2073                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2074                        [point, ConstrainablePoint2dOrOrigin::Origin]
2075                    } else {
2076                        [ConstrainablePoint2dOrOrigin::Origin, point]
2077                    };
2078                    Ok(KclValue::SketchConstraint {
2079                        value: Box::new(SketchConstraint {
2080                            kind: SketchConstraintKind::HorizontalDistance { points },
2081                            meta: vec![args.source_range.into()],
2082                        }),
2083                    })
2084                }
2085                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2086                    "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
2087                        .to_owned(),
2088                    vec![args.source_range],
2089                ))),
2090            }
2091        }
2092        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2093            "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
2094            vec![args.source_range],
2095        ))),
2096    }
2097}
2098
2099pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2100    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2101        "points",
2102        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2103        exec_state,
2104    )?;
2105    let [p1, p2] = points.as_slice() else {
2106        return Err(KclError::new_semantic(KclErrorDetails::new(
2107            "must have two input points".to_owned(),
2108            vec![args.source_range],
2109        )));
2110    };
2111    match (p1, p2) {
2112        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2113            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2114                return Err(KclError::new_semantic(KclErrorDetails::new(
2115                    "first point must be an unsolved segment".to_owned(),
2116                    vec![args.source_range],
2117                )));
2118            };
2119            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2120                return Err(KclError::new_semantic(KclErrorDetails::new(
2121                    "second point must be an unsolved segment".to_owned(),
2122                    vec![args.source_range],
2123                )));
2124            };
2125            match (&unsolved0.kind, &unsolved1.kind) {
2126                (
2127                    UnsolvedSegmentKind::Point { position: pos0, .. },
2128                    UnsolvedSegmentKind::Point { position: pos1, .. },
2129                ) => {
2130                    // Both segments are points. Create a vertical distance constraint
2131                    // between them.
2132                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2133                        (
2134                            UnsolvedExpr::Unknown(p0_x),
2135                            UnsolvedExpr::Unknown(p0_y),
2136                            UnsolvedExpr::Unknown(p1_x),
2137                            UnsolvedExpr::Unknown(p1_y),
2138                        ) => {
2139                            // All coordinates are sketch vars. Proceed.
2140                            let sketch_constraint = SketchConstraint {
2141                                kind: SketchConstraintKind::VerticalDistance {
2142                                    points: [
2143                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2144                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2145                                            object_id: unsolved0.object_id,
2146                                        }),
2147                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2148                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2149                                            object_id: unsolved1.object_id,
2150                                        }),
2151                                    ],
2152                                },
2153                                meta: vec![args.source_range.into()],
2154                            };
2155                            Ok(KclValue::SketchConstraint {
2156                                value: Box::new(sketch_constraint),
2157                            })
2158                        }
2159                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2160                            "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
2161                                .to_owned(),
2162                            vec![args.source_range],
2163                        ))),
2164                    }
2165                }
2166                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2167                    "verticalDistance() arguments must be unsolved points".to_owned(),
2168                    vec![args.source_range],
2169                ))),
2170            }
2171        }
2172        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2173            if !point2d_is_origin(point2d) {
2174                return Err(KclError::new_semantic(KclErrorDetails::new(
2175                    "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
2176                    vec![args.source_range],
2177                )));
2178            }
2179
2180            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2181                return Err(KclError::new_semantic(KclErrorDetails::new(
2182                    "segment must be an unsolved segment".to_owned(),
2183                    vec![args.source_range],
2184                )));
2185            };
2186            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2187                return Err(KclError::new_semantic(KclErrorDetails::new(
2188                    "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2189                    vec![args.source_range],
2190                )));
2191            };
2192            match (&position[0], &position[1]) {
2193                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2194                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2195                        vars: crate::front::Point2d {
2196                            x: *point_x,
2197                            y: *point_y,
2198                        },
2199                        object_id: unsolved.object_id,
2200                    });
2201                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2202                        [point, ConstrainablePoint2dOrOrigin::Origin]
2203                    } else {
2204                        [ConstrainablePoint2dOrOrigin::Origin, point]
2205                    };
2206                    Ok(KclValue::SketchConstraint {
2207                        value: Box::new(SketchConstraint {
2208                            kind: SketchConstraintKind::VerticalDistance { points },
2209                            meta: vec![args.source_range.into()],
2210                        }),
2211                    })
2212                }
2213                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2214                    "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
2215                        .to_owned(),
2216                    vec![args.source_range],
2217                ))),
2218            }
2219        }
2220        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2221            "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
2222            vec![args.source_range],
2223        ))),
2224    }
2225}
2226
2227pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2228    #[derive(Clone, Copy)]
2229    struct ConstrainableLine {
2230        solver_line: DatumLineSegment,
2231        #[cfg(feature = "artifact-graph")]
2232        object_id: ObjectId,
2233    }
2234
2235    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2236        "lines",
2237        &RuntimeType::Array(
2238            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2239            ArrayLen::Minimum(2),
2240        ),
2241        exec_state,
2242    )?;
2243    let range = args.source_range;
2244    let constrainable_lines: Vec<ConstrainableLine> = lines
2245        .iter()
2246        .map(|line| {
2247            let KclValue::Segment { value: segment } = line else {
2248                return Err(KclError::new_semantic(KclErrorDetails::new(
2249                    "line argument must be a Segment".to_owned(),
2250                    vec![args.source_range],
2251                )));
2252            };
2253            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2254                return Err(KclError::new_internal(KclErrorDetails::new(
2255                    "line must be an unsolved Segment".to_owned(),
2256                    vec![args.source_range],
2257                )));
2258            };
2259            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2260                return Err(KclError::new_semantic(KclErrorDetails::new(
2261                    "line argument must be a line, no other type of Segment".to_owned(),
2262                    vec![args.source_range],
2263                )));
2264            };
2265            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2266                return Err(KclError::new_semantic(KclErrorDetails::new(
2267                    "line's start x coordinate must be a var".to_owned(),
2268                    vec![args.source_range],
2269                )));
2270            };
2271            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2272                return Err(KclError::new_semantic(KclErrorDetails::new(
2273                    "line's start y coordinate must be a var".to_owned(),
2274                    vec![args.source_range],
2275                )));
2276            };
2277            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2278                return Err(KclError::new_semantic(KclErrorDetails::new(
2279                    "line's end x coordinate must be a var".to_owned(),
2280                    vec![args.source_range],
2281                )));
2282            };
2283            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2284                return Err(KclError::new_semantic(KclErrorDetails::new(
2285                    "line's end y coordinate must be a var".to_owned(),
2286                    vec![args.source_range],
2287                )));
2288            };
2289
2290            let solver_line_p0 =
2291                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2292            let solver_line_p1 =
2293                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2294
2295            Ok(ConstrainableLine {
2296                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2297                #[cfg(feature = "artifact-graph")]
2298                object_id: unsolved.object_id,
2299            })
2300        })
2301        .collect::<Result<_, _>>()?;
2302
2303    #[cfg(feature = "artifact-graph")]
2304    let constraint_id = exec_state.next_object_id();
2305    // Save the constraint to be used for solving.
2306    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2307        return Err(KclError::new_semantic(KclErrorDetails::new(
2308            "equalLength() can only be used inside a sketch block".to_owned(),
2309            vec![args.source_range],
2310        )));
2311    };
2312    let first_line = constrainable_lines[0];
2313    for line in constrainable_lines.iter().skip(1) {
2314        sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
2315            first_line.solver_line,
2316            line.solver_line,
2317        ));
2318    }
2319    #[cfg(feature = "artifact-graph")]
2320    {
2321        let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
2322            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
2323        });
2324        sketch_state.sketch_constraints.push(constraint_id);
2325        track_constraint(constraint_id, constraint, exec_state, &args);
2326    }
2327    Ok(KclValue::none())
2328}
2329
2330fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
2331    Ok(DatumPoint::new_xy(
2332        coords[0].to_constraint_id(range)?,
2333        coords[1].to_constraint_id(range)?,
2334    ))
2335}
2336
2337fn sketch_var_initial_value(
2338    sketch_vars: &[KclValue],
2339    id: SketchVarId,
2340    exec_state: &mut ExecState,
2341    range: crate::SourceRange,
2342) -> Result<f64, KclError> {
2343    sketch_vars
2344        .get(id.0)
2345        .and_then(KclValue::as_sketch_var)
2346        .map(|sketch_var| {
2347            sketch_var
2348                .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
2349                .map(|value| value.n)
2350        })
2351        .transpose()?
2352        .ok_or_else(|| {
2353            KclError::new_internal(KclErrorDetails::new(
2354                format!("Missing sketch variable initial value for id {}", id.0),
2355                vec![range],
2356            ))
2357        })
2358}
2359
2360fn radius_guess(
2361    sketch_vars: &[KclValue],
2362    center: [SketchVarId; 2],
2363    point: [SketchVarId; 2],
2364    exec_state: &mut ExecState,
2365    range: crate::SourceRange,
2366) -> Result<f64, KclError> {
2367    let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
2368        - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
2369    let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
2370        - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
2371    Ok(libm::hypot(dx, dy))
2372}
2373
2374pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2375    #[derive(Debug, Clone, Copy)]
2376    struct RadiusInputVars {
2377        center: [SketchVarId; 2],
2378        start: [SketchVarId; 2],
2379        end: Option<[SketchVarId; 2]>,
2380    }
2381
2382    #[derive(Debug, Clone, Copy)]
2383    enum EqualRadiusInput {
2384        Radius(RadiusInputVars),
2385    }
2386
2387    fn extract_equal_radius_input(
2388        segment_value: &KclValue,
2389        range: crate::SourceRange,
2390    ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
2391        let KclValue::Segment { value: segment } = segment_value else {
2392            return Err(KclError::new_semantic(KclErrorDetails::new(
2393                format!(
2394                    "equalRadius() arguments must be segments but found {}",
2395                    segment_value.human_friendly_type()
2396                ),
2397                vec![range],
2398            )));
2399        };
2400        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2401            return Err(KclError::new_semantic(KclErrorDetails::new(
2402                "equalRadius() arguments must be unsolved segments".to_owned(),
2403                vec![range],
2404            )));
2405        };
2406        match &unsolved.kind {
2407            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2408                let (
2409                    UnsolvedExpr::Unknown(center_x),
2410                    UnsolvedExpr::Unknown(center_y),
2411                    UnsolvedExpr::Unknown(start_x),
2412                    UnsolvedExpr::Unknown(start_y),
2413                    UnsolvedExpr::Unknown(end_x),
2414                    UnsolvedExpr::Unknown(end_y),
2415                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2416                else {
2417                    return Err(KclError::new_semantic(KclErrorDetails::new(
2418                        "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
2419                        vec![range],
2420                    )));
2421                };
2422                Ok((
2423                    EqualRadiusInput::Radius(RadiusInputVars {
2424                        center: [*center_x, *center_y],
2425                        start: [*start_x, *start_y],
2426                        end: Some([*end_x, *end_y]),
2427                    }),
2428                    unsolved.object_id,
2429                ))
2430            }
2431            UnsolvedSegmentKind::Circle { center, start, .. } => {
2432                let (
2433                    UnsolvedExpr::Unknown(center_x),
2434                    UnsolvedExpr::Unknown(center_y),
2435                    UnsolvedExpr::Unknown(start_x),
2436                    UnsolvedExpr::Unknown(start_y),
2437                ) = (&center[0], &center[1], &start[0], &start[1])
2438                else {
2439                    return Err(KclError::new_semantic(KclErrorDetails::new(
2440                        "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
2441                        vec![range],
2442                    )));
2443                };
2444                Ok((
2445                    EqualRadiusInput::Radius(RadiusInputVars {
2446                        center: [*center_x, *center_y],
2447                        start: [*start_x, *start_y],
2448                        end: None,
2449                    }),
2450                    unsolved.object_id,
2451                ))
2452            }
2453            other => Err(KclError::new_semantic(KclErrorDetails::new(
2454                format!(
2455                    "equalRadius() currently supports only arc and circle segments, you provided {}",
2456                    other.human_friendly_kind_with_article()
2457                ),
2458                vec![range],
2459            ))),
2460        }
2461    }
2462
2463    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2464        "input",
2465        &RuntimeType::Array(
2466            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2467            ArrayLen::Minimum(2),
2468        ),
2469        exec_state,
2470    )?;
2471    let range = args.source_range;
2472
2473    let extracted_input = input
2474        .iter()
2475        .map(|segment_value| extract_equal_radius_input(segment_value, range))
2476        .collect::<Result<Vec<_>, _>>()?;
2477    let radius_inputs: Vec<RadiusInputVars> = extracted_input
2478        .iter()
2479        .map(|(equal_radius_input, _)| match equal_radius_input {
2480            EqualRadiusInput::Radius(radius_input) => *radius_input,
2481        })
2482        .collect();
2483    #[cfg(feature = "artifact-graph")]
2484    let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
2485
2486    let sketch_var_ty = solver_numeric_type(exec_state);
2487    #[cfg(feature = "artifact-graph")]
2488    let constraint_id = exec_state.next_object_id();
2489
2490    let sketch_vars = {
2491        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2492            return Err(KclError::new_semantic(KclErrorDetails::new(
2493                "equalRadius() can only be used inside a sketch block".to_owned(),
2494                vec![range],
2495            )));
2496        };
2497        sketch_state.sketch_vars.clone()
2498    };
2499
2500    let radius_initial_value = radius_guess(
2501        &sketch_vars,
2502        radius_inputs[0].center,
2503        radius_inputs[0].start,
2504        exec_state,
2505        range,
2506    )?;
2507
2508    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2509        return Err(KclError::new_semantic(KclErrorDetails::new(
2510            "equalRadius() can only be used inside a sketch block".to_owned(),
2511            vec![range],
2512        )));
2513    };
2514    let radius_id = sketch_state.next_sketch_var_id();
2515    sketch_state.sketch_vars.push(KclValue::SketchVar {
2516        value: Box::new(crate::execution::SketchVar {
2517            id: radius_id,
2518            initial_value: radius_initial_value,
2519            ty: sketch_var_ty,
2520            meta: vec![],
2521        }),
2522    });
2523    let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2524
2525    for radius_input in radius_inputs {
2526        let center = datum_point(radius_input.center, range)?;
2527        let start = datum_point(radius_input.start, range)?;
2528        sketch_state
2529            .solver_constraints
2530            .push(SolverConstraint::DistanceVar(start, center, radius));
2531        if let Some(end) = radius_input.end {
2532            let end = datum_point(end, range)?;
2533            sketch_state
2534                .solver_constraints
2535                .push(SolverConstraint::DistanceVar(end, center, radius));
2536        }
2537    }
2538
2539    #[cfg(feature = "artifact-graph")]
2540    {
2541        let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
2542            input: input_object_ids,
2543        });
2544        sketch_state.sketch_constraints.push(constraint_id);
2545        track_constraint(constraint_id, constraint, exec_state, &args);
2546    }
2547
2548    Ok(KclValue::none())
2549}
2550
2551pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2552    #[derive(Debug, Clone, Copy)]
2553    struct ConstrainableLineVars {
2554        start: [SketchVarId; 2],
2555        end: [SketchVarId; 2],
2556    }
2557
2558    #[derive(Debug, Clone, Copy)]
2559    struct ConstrainableCircularVars {
2560        center: [SketchVarId; 2],
2561        start: [SketchVarId; 2],
2562        end: Option<[SketchVarId; 2]>,
2563    }
2564
2565    #[derive(Debug, Clone, Copy)]
2566    enum TangentInput {
2567        Line(ConstrainableLineVars),
2568        Circular(ConstrainableCircularVars),
2569    }
2570
2571    fn extract_tangent_input(
2572        segment_value: &KclValue,
2573        range: crate::SourceRange,
2574    ) -> Result<(TangentInput, ObjectId), KclError> {
2575        let KclValue::Segment { value: segment } = segment_value else {
2576            return Err(KclError::new_semantic(KclErrorDetails::new(
2577                "tangent() arguments must be segments".to_owned(),
2578                vec![range],
2579            )));
2580        };
2581        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2582            return Err(KclError::new_semantic(KclErrorDetails::new(
2583                "tangent() arguments must be unsolved segments".to_owned(),
2584                vec![range],
2585            )));
2586        };
2587        match &unsolved.kind {
2588            UnsolvedSegmentKind::Line { start, end, .. } => {
2589                let (
2590                    UnsolvedExpr::Unknown(start_x),
2591                    UnsolvedExpr::Unknown(start_y),
2592                    UnsolvedExpr::Unknown(end_x),
2593                    UnsolvedExpr::Unknown(end_y),
2594                ) = (&start[0], &start[1], &end[0], &end[1])
2595                else {
2596                    return Err(KclError::new_semantic(KclErrorDetails::new(
2597                        "line coordinates must be sketch vars for tangent()".to_owned(),
2598                        vec![range],
2599                    )));
2600                };
2601                Ok((
2602                    TangentInput::Line(ConstrainableLineVars {
2603                        start: [*start_x, *start_y],
2604                        end: [*end_x, *end_y],
2605                    }),
2606                    unsolved.object_id,
2607                ))
2608            }
2609            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2610                let (
2611                    UnsolvedExpr::Unknown(center_x),
2612                    UnsolvedExpr::Unknown(center_y),
2613                    UnsolvedExpr::Unknown(start_x),
2614                    UnsolvedExpr::Unknown(start_y),
2615                    UnsolvedExpr::Unknown(end_x),
2616                    UnsolvedExpr::Unknown(end_y),
2617                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2618                else {
2619                    return Err(KclError::new_semantic(KclErrorDetails::new(
2620                        "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
2621                        vec![range],
2622                    )));
2623                };
2624                Ok((
2625                    TangentInput::Circular(ConstrainableCircularVars {
2626                        center: [*center_x, *center_y],
2627                        start: [*start_x, *start_y],
2628                        end: Some([*end_x, *end_y]),
2629                    }),
2630                    unsolved.object_id,
2631                ))
2632            }
2633            UnsolvedSegmentKind::Circle { center, start, .. } => {
2634                let (
2635                    UnsolvedExpr::Unknown(center_x),
2636                    UnsolvedExpr::Unknown(center_y),
2637                    UnsolvedExpr::Unknown(start_x),
2638                    UnsolvedExpr::Unknown(start_y),
2639                ) = (&center[0], &center[1], &start[0], &start[1])
2640                else {
2641                    return Err(KclError::new_semantic(KclErrorDetails::new(
2642                        "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
2643                        vec![range],
2644                    )));
2645                };
2646                Ok((
2647                    TangentInput::Circular(ConstrainableCircularVars {
2648                        center: [*center_x, *center_y],
2649                        start: [*start_x, *start_y],
2650                        end: None,
2651                    }),
2652                    unsolved.object_id,
2653                ))
2654            }
2655            _ => Err(KclError::new_semantic(KclErrorDetails::new(
2656                "tangent() supports only line, arc, and circle segments".to_owned(),
2657                vec![range],
2658            ))),
2659        }
2660    }
2661
2662    fn point_initial_position(
2663        sketch_vars: &[KclValue],
2664        point: [SketchVarId; 2],
2665        exec_state: &mut ExecState,
2666        range: crate::SourceRange,
2667    ) -> Result<[f64; 2], KclError> {
2668        Ok([
2669            sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
2670            sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
2671        ])
2672    }
2673
2674    fn canonicalize_line_for_tangent(
2675        sketch_vars: &[KclValue],
2676        line: ConstrainableLineVars,
2677        arc_center: [SketchVarId; 2],
2678        exec_state: &mut ExecState,
2679        range: crate::SourceRange,
2680    ) -> Result<ConstrainableLineVars, KclError> {
2681        let [sx, sy] = point_initial_position(sketch_vars, line.start, exec_state, range)?;
2682        let [ex, ey] = point_initial_position(sketch_vars, line.end, exec_state, range)?;
2683        let [cx, cy] = point_initial_position(sketch_vars, arc_center, exec_state, range)?;
2684
2685        // Canonicalize the line orientation so LineTangentToCircle sees the arc
2686        // center on its non-negative side regardless of how the user ordered the
2687        // line endpoints in KCL.
2688        let signed_side = (ex - sx) * (cy - sy) - (ey - sy) * (cx - sx);
2689        if signed_side < -1e-9 {
2690            Ok(ConstrainableLineVars {
2691                start: line.end,
2692                end: line.start,
2693            })
2694        } else {
2695            Ok(line)
2696        }
2697    }
2698
2699    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2700        "input",
2701        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2702        exec_state,
2703    )?;
2704    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
2705        KclError::new_semantic(KclErrorDetails::new(
2706            "tangent() requires exactly 2 input segments".to_owned(),
2707            vec![args.source_range],
2708        ))
2709    })?;
2710    let range = args.source_range;
2711    let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
2712    let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
2713    #[cfg(not(feature = "artifact-graph"))]
2714    let _ = (input0_object_id, input1_object_id);
2715
2716    enum TangentCase {
2717        LineCircular(ConstrainableLineVars, ConstrainableCircularVars),
2718        CircularCircular(ConstrainableCircularVars, ConstrainableCircularVars),
2719    }
2720    let tangent_case = match (input0, input1) {
2721        (TangentInput::Line(line), TangentInput::Circular(circular))
2722        | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
2723        (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
2724            TangentCase::CircularCircular(circular0, circular1)
2725        }
2726        (TangentInput::Line(_), TangentInput::Line(_)) => {
2727            return Err(KclError::new_semantic(KclErrorDetails::new(
2728                "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
2729                vec![range],
2730            )));
2731        }
2732    };
2733
2734    let sketch_var_ty = solver_numeric_type(exec_state);
2735    #[cfg(feature = "artifact-graph")]
2736    let constraint_id = exec_state.next_object_id();
2737
2738    let sketch_vars = {
2739        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2740            return Err(KclError::new_semantic(KclErrorDetails::new(
2741                "tangent() can only be used inside a sketch block".to_owned(),
2742                vec![range],
2743            )));
2744        };
2745        sketch_state.sketch_vars.clone()
2746    };
2747
2748    // Hidden radius vars. Empty metadata keeps them out of source write-back.
2749    match tangent_case {
2750        TangentCase::LineCircular(line, circular) => {
2751            let canonical_line = canonicalize_line_for_tangent(&sketch_vars, line, circular.center, exec_state, range)?;
2752            let line_p0 = datum_point(canonical_line.start, range)?;
2753            let line_p1 = datum_point(canonical_line.end, range)?;
2754            let line_datum = DatumLineSegment::new(line_p0, line_p1);
2755
2756            let center = datum_point(circular.center, range)?;
2757            let circular_start = datum_point(circular.start, range)?;
2758            let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
2759            let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
2760            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2761                return Err(KclError::new_semantic(KclErrorDetails::new(
2762                    "tangent() can only be used inside a sketch block".to_owned(),
2763                    vec![range],
2764                )));
2765            };
2766            let radius_id = sketch_state.next_sketch_var_id();
2767            sketch_state.sketch_vars.push(KclValue::SketchVar {
2768                value: Box::new(crate::execution::SketchVar {
2769                    id: radius_id,
2770                    initial_value: radius_initial_value,
2771                    ty: sketch_var_ty,
2772                    meta: vec![],
2773                }),
2774            });
2775            let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2776            let circle = DatumCircle { center, radius };
2777
2778            // Tangency decomposition for Line/circular segment:
2779            // 1) Introduce a hidden radius variable r for the segment's underlying circle.
2780            // 2) Keep the segment's defining points on that circle with DistanceVar(point, center, r).
2781            // 3) Canonicalize the solver line orientation so endpoint order
2782            //    doesn't change the tangent branch.
2783            // 4) Apply the native LineTangentToCircle solver constraint.
2784            sketch_state
2785                .solver_constraints
2786                .push(SolverConstraint::DistanceVar(circular_start, center, radius));
2787            if let Some(circular_end) = circular_end {
2788                sketch_state
2789                    .solver_constraints
2790                    .push(SolverConstraint::DistanceVar(circular_end, center, radius));
2791            }
2792            sketch_state
2793                .solver_constraints
2794                .push(SolverConstraint::LineTangentToCircle(line_datum, circle));
2795        }
2796        TangentCase::CircularCircular(circular0, circular1) => {
2797            let center0 = datum_point(circular0.center, range)?;
2798            let start0 = datum_point(circular0.start, range)?;
2799            let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
2800            let radius0_initial_value =
2801                radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
2802            let center1 = datum_point(circular1.center, range)?;
2803            let start1 = datum_point(circular1.start, range)?;
2804            let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
2805            let radius1_initial_value =
2806                radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
2807            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2808                return Err(KclError::new_semantic(KclErrorDetails::new(
2809                    "tangent() can only be used inside a sketch block".to_owned(),
2810                    vec![range],
2811                )));
2812            };
2813            let radius0_id = sketch_state.next_sketch_var_id();
2814            sketch_state.sketch_vars.push(KclValue::SketchVar {
2815                value: Box::new(crate::execution::SketchVar {
2816                    id: radius0_id,
2817                    initial_value: radius0_initial_value,
2818                    ty: sketch_var_ty,
2819                    meta: vec![],
2820                }),
2821            });
2822            let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
2823            let circle0 = DatumCircle {
2824                center: center0,
2825                radius: radius0,
2826            };
2827
2828            let radius1_id = sketch_state.next_sketch_var_id();
2829            sketch_state.sketch_vars.push(KclValue::SketchVar {
2830                value: Box::new(crate::execution::SketchVar {
2831                    id: radius1_id,
2832                    initial_value: radius1_initial_value,
2833                    ty: sketch_var_ty,
2834                    meta: vec![],
2835                }),
2836            });
2837            let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
2838            let circle1 = DatumCircle {
2839                center: center1,
2840                radius: radius1,
2841            };
2842
2843            // Tangency decomposition for circular segment/circular segment:
2844            // 1) Introduce one hidden radius variable per arc.
2845            // 2) Keep each segment's defining points on its corresponding circle.
2846            // 3) Apply the native CircleTangentToCircle solver constraint.
2847            sketch_state
2848                .solver_constraints
2849                .push(SolverConstraint::DistanceVar(start0, center0, radius0));
2850            if let Some(end0) = end0 {
2851                sketch_state
2852                    .solver_constraints
2853                    .push(SolverConstraint::DistanceVar(end0, center0, radius0));
2854            }
2855            sketch_state
2856                .solver_constraints
2857                .push(SolverConstraint::DistanceVar(start1, center1, radius1));
2858            if let Some(end1) = end1 {
2859                sketch_state
2860                    .solver_constraints
2861                    .push(SolverConstraint::DistanceVar(end1, center1, radius1));
2862            }
2863            sketch_state
2864                .solver_constraints
2865                .push(SolverConstraint::CircleTangentToCircle(circle0, circle1));
2866        }
2867    }
2868
2869    #[cfg(feature = "artifact-graph")]
2870    {
2871        let constraint = crate::front::Constraint::Tangent(Tangent {
2872            input: vec![input0_object_id, input1_object_id],
2873        });
2874        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2875            return Err(KclError::new_semantic(KclErrorDetails::new(
2876                "tangent() can only be used inside a sketch block".to_owned(),
2877                vec![range],
2878            )));
2879        };
2880        sketch_state.sketch_constraints.push(constraint_id);
2881        track_constraint(constraint_id, constraint, exec_state, &args);
2882    }
2883
2884    Ok(KclValue::none())
2885}
2886
2887#[derive(Debug, Clone, Copy)]
2888pub(crate) enum LinesAtAngleKind {
2889    Parallel,
2890    Perpendicular,
2891}
2892
2893impl LinesAtAngleKind {
2894    pub fn to_function_name(self) -> &'static str {
2895        match self {
2896            LinesAtAngleKind::Parallel => "parallel",
2897            LinesAtAngleKind::Perpendicular => "perpendicular",
2898        }
2899    }
2900
2901    fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
2902        match self {
2903            LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
2904            LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
2905        }
2906    }
2907
2908    #[cfg(feature = "artifact-graph")]
2909    fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
2910        match self {
2911            LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
2912            LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
2913        }
2914    }
2915}
2916
2917/// Convert between two different libraries with similar angle representations
2918#[expect(unused)]
2919fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
2920    kcmc::shared::Angle::from_degrees(angle.to_degrees())
2921}
2922
2923/// Convert between two different libraries with similar angle representations
2924#[expect(unused)]
2925fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
2926    ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
2927}
2928
2929pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2930    #[derive(Clone, Copy)]
2931    struct ConstrainableLine {
2932        solver_line: DatumLineSegment,
2933        #[cfg(feature = "artifact-graph")]
2934        object_id: ObjectId,
2935    }
2936
2937    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2938        "lines",
2939        &RuntimeType::Array(
2940            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2941            ArrayLen::Minimum(2),
2942        ),
2943        exec_state,
2944    )?;
2945    let range = args.source_range;
2946    let constrainable_lines: Vec<ConstrainableLine> = lines
2947        .iter()
2948        .map(|line| {
2949            let KclValue::Segment { value: segment } = line else {
2950                return Err(KclError::new_semantic(KclErrorDetails::new(
2951                    "line argument must be a Segment".to_owned(),
2952                    vec![args.source_range],
2953                )));
2954            };
2955            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2956                return Err(KclError::new_internal(KclErrorDetails::new(
2957                    "line must be an unsolved Segment".to_owned(),
2958                    vec![args.source_range],
2959                )));
2960            };
2961            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2962                return Err(KclError::new_semantic(KclErrorDetails::new(
2963                    "line argument must be a line, no other type of Segment".to_owned(),
2964                    vec![args.source_range],
2965                )));
2966            };
2967            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2968                return Err(KclError::new_semantic(KclErrorDetails::new(
2969                    "line's start x coordinate must be a var".to_owned(),
2970                    vec![args.source_range],
2971                )));
2972            };
2973            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2974                return Err(KclError::new_semantic(KclErrorDetails::new(
2975                    "line's start y coordinate must be a var".to_owned(),
2976                    vec![args.source_range],
2977                )));
2978            };
2979            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2980                return Err(KclError::new_semantic(KclErrorDetails::new(
2981                    "line's end x coordinate must be a var".to_owned(),
2982                    vec![args.source_range],
2983                )));
2984            };
2985            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2986                return Err(KclError::new_semantic(KclErrorDetails::new(
2987                    "line's end y coordinate must be a var".to_owned(),
2988                    vec![args.source_range],
2989                )));
2990            };
2991
2992            let solver_line_p0 =
2993                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2994            let solver_line_p1 =
2995                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2996
2997            Ok(ConstrainableLine {
2998                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2999                #[cfg(feature = "artifact-graph")]
3000                object_id: unsolved.object_id,
3001            })
3002        })
3003        .collect::<Result<_, _>>()?;
3004
3005    #[cfg(feature = "artifact-graph")]
3006    let constraint_id = exec_state.next_object_id();
3007    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3008        return Err(KclError::new_semantic(KclErrorDetails::new(
3009            "parallel() can only be used inside a sketch block".to_owned(),
3010            vec![args.source_range],
3011        )));
3012    };
3013
3014    let n = constrainable_lines.len();
3015    let mut constrainable_lines_iter = constrainable_lines.iter();
3016    let first_line = constrainable_lines_iter
3017        .next()
3018        .ok_or(KclError::new_semantic(KclErrorDetails::new(
3019            format!("parallel() requires at least 2 lines, but you provided {}", n),
3020            vec![args.source_range],
3021        )))?;
3022    for line in constrainable_lines_iter {
3023        sketch_state.solver_constraints.push(SolverConstraint::LinesAtAngle(
3024            first_line.solver_line,
3025            line.solver_line,
3026            AngleKind::Parallel,
3027        ));
3028    }
3029    #[cfg(feature = "artifact-graph")]
3030    {
3031        let constraint = Constraint::Parallel(Parallel {
3032            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
3033        });
3034        sketch_state.sketch_constraints.push(constraint_id);
3035        track_constraint(constraint_id, constraint, exec_state, &args);
3036    }
3037    Ok(KclValue::none())
3038}
3039
3040pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3041    lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
3042}
3043
3044/// A way to constrain points, or a line.
3045#[derive(Debug, Clone, Copy)]
3046enum AxisConstraintKind {
3047    Horizontal,
3048    Vertical,
3049}
3050
3051impl AxisConstraintKind {
3052    /// Which KCL function this corresponds to.
3053    fn function_name(self) -> &'static str {
3054        match self {
3055            AxisConstraintKind::Horizontal => "horizontal",
3056            AxisConstraintKind::Vertical => "vertical",
3057        }
3058    }
3059
3060    /// Use this constraint to align a line.
3061    fn line_constraint(self, line: DatumLineSegment) -> SolverConstraint {
3062        match self {
3063            AxisConstraintKind::Horizontal => SolverConstraint::Horizontal(line),
3064            AxisConstraintKind::Vertical => SolverConstraint::Vertical(line),
3065        }
3066    }
3067
3068    /// Use this constraint to align a pair of points.
3069    fn point_pair_constraint(self, p0: DatumPoint, p1: DatumPoint) -> SolverConstraint {
3070        match self {
3071            // A horizontal point set means all Y values are equal.
3072            AxisConstraintKind::Horizontal => SolverConstraint::VerticalDistance(p1, p0, 0.0),
3073            // A vertical point set means all X values are equal.
3074            AxisConstraintKind::Vertical => SolverConstraint::HorizontalDistance(p1, p0, 0.0),
3075        }
3076    }
3077
3078    /// Use this constraint to align a point to some known X or Y.
3079    fn constraint_aligning_point_to_constant(self, p0: DatumPoint, fixed_point: (f64, f64)) -> SolverConstraint {
3080        match self {
3081            AxisConstraintKind::Horizontal => SolverConstraint::Fixed(p0.y_id, fixed_point.1),
3082            AxisConstraintKind::Vertical => SolverConstraint::Fixed(p0.x_id, fixed_point.0),
3083        }
3084    }
3085
3086    #[cfg(feature = "artifact-graph")]
3087    fn line_artifact_constraint(self, line: ObjectId) -> Constraint {
3088        match self {
3089            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Line { line }),
3090            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Line { line }),
3091        }
3092    }
3093
3094    #[cfg(feature = "artifact-graph")]
3095    fn point_artifact_constraint(self, points: Vec<ConstraintSegment>) -> Constraint {
3096        match self {
3097            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Points { points }),
3098            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Points { points }),
3099        }
3100    }
3101}
3102
3103/// The line the user wants to align vertically/horizontally.
3104/// Extracted from KCL arguments.
3105#[derive(Debug, Clone, Copy)]
3106struct AxisLineVars {
3107    start: [SketchVarId; 2],
3108    end: [SketchVarId; 2],
3109    object_id: ObjectId,
3110}
3111
3112fn extract_axis_line_vars(
3113    segment: &AbstractSegment,
3114    kind: AxisConstraintKind,
3115    source_range: crate::SourceRange,
3116) -> Result<AxisLineVars, KclError> {
3117    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3118        return Err(KclError::new_internal(KclErrorDetails::new(
3119            "line must be an unsolved Segment".to_owned(),
3120            vec![source_range],
3121        )));
3122    };
3123    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3124        return Err(KclError::new_semantic(KclErrorDetails::new(
3125            format!(
3126                "{}() line argument must be a line, no other type of Segment",
3127                kind.function_name()
3128            ),
3129            vec![source_range],
3130        )));
3131    };
3132    let (
3133        UnsolvedExpr::Unknown(start_x),
3134        UnsolvedExpr::Unknown(start_y),
3135        UnsolvedExpr::Unknown(end_x),
3136        UnsolvedExpr::Unknown(end_y),
3137    ) = (&start[0], &start[1], &end[0], &end[1])
3138    else {
3139        return Err(KclError::new_semantic(KclErrorDetails::new(
3140            "line's x and y coordinates of both start and end must be vars".to_owned(),
3141            vec![source_range],
3142        )));
3143    };
3144
3145    Ok(AxisLineVars {
3146        start: [*start_x, *start_y],
3147        end: [*end_x, *end_y],
3148        object_id: unsolved.object_id,
3149    })
3150}
3151
3152#[derive(Debug, Clone)]
3153enum PointToAlign {
3154    /// Variable point that could be constrained.
3155    Variable { x: SketchVarId, y: SketchVarId },
3156    /// Fixed millimeter constant.
3157    Fixed { x: TyF64, y: TyF64 },
3158}
3159
3160impl From<[SketchVarId; 2]> for PointToAlign {
3161    fn from(sketch_var: [SketchVarId; 2]) -> Self {
3162        Self::Variable {
3163            x: sketch_var[0],
3164            y: sketch_var[1],
3165        }
3166    }
3167}
3168
3169impl From<[TyF64; 2]> for PointToAlign {
3170    fn from([x, y]: [TyF64; 2]) -> Self {
3171        Self::Fixed { x, y }
3172    }
3173}
3174
3175fn extract_axis_point_vars(
3176    input: &KclValue,
3177    kind: AxisConstraintKind,
3178    source_range: crate::SourceRange,
3179) -> Result<PointToAlign, KclError> {
3180    match input {
3181        KclValue::Segment { value: segment } => {
3182            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3183                return Err(KclError::new_semantic(KclErrorDetails::new(
3184                    format!(
3185                        "The `{}` function point arguments must be unsolved points",
3186                        kind.function_name()
3187                    ),
3188                    vec![source_range],
3189                )));
3190            };
3191            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
3192                return Err(KclError::new_semantic(KclErrorDetails::new(
3193                    format!(
3194                        "The `{}` function list arguments must be points, but one item is {}",
3195                        kind.function_name(),
3196                        unsolved.kind.human_friendly_kind_with_article()
3197                    ),
3198                    vec![source_range],
3199                )));
3200            };
3201            match (&position[0], &position[1]) {
3202                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed {
3203                    x: x.to_owned(),
3204                    y: y.to_owned(),
3205                }),
3206                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x: *x, y: *y }),
3207                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
3208                    Err(KclError::new_semantic(KclErrorDetails::new(
3209                        format!(
3210                            "The `{}` function cannot take a fixed X component and a variable Y component",
3211                            kind.function_name()
3212                        ),
3213                        vec![source_range],
3214                    )))
3215                }
3216                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
3217                    Err(KclError::new_semantic(KclErrorDetails::new(
3218                        format!(
3219                            "The `{}` function cannot take a fixed X component and a variable Y component",
3220                            kind.function_name()
3221                        ),
3222                        vec![source_range],
3223                    )))
3224                }
3225            }
3226        }
3227        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
3228            let [x_value, y_value] = value.as_slice() else {
3229                return Err(KclError::new_semantic(KclErrorDetails::new(
3230                    format!(
3231                        "The `{}` function point arguments must each be a Point2d like [var 0mm, var 0mm]",
3232                        kind.function_name()
3233                    ),
3234                    vec![source_range],
3235                )));
3236            };
3237            let Some(x_expr) = x_value.as_unsolved_expr() else {
3238                return Err(KclError::new_semantic(KclErrorDetails::new(
3239                    format!(
3240                        "The `{}` function point x coordinate must be a number or sketch var",
3241                        kind.function_name()
3242                    ),
3243                    vec![source_range],
3244                )));
3245            };
3246            let Some(y_expr) = y_value.as_unsolved_expr() else {
3247                return Err(KclError::new_semantic(KclErrorDetails::new(
3248                    format!(
3249                        "The `{}` function point y coordinate must be a number or sketch var",
3250                        kind.function_name()
3251                    ),
3252                    vec![source_range],
3253                )));
3254            };
3255            match (x_expr, y_expr) {
3256                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed { x, y }),
3257                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x, y }),
3258                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
3259                    Err(KclError::new_semantic(KclErrorDetails::new(
3260                        format!(
3261                            "The `{}` function cannot take a fixed X component and a variable Y component",
3262                            kind.function_name()
3263                        ),
3264                        vec![source_range],
3265                    )))
3266                }
3267                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
3268                    Err(KclError::new_semantic(KclErrorDetails::new(
3269                        format!(
3270                            "The `{}` function cannot take a fixed X component and a variable Y component",
3271                            kind.function_name()
3272                        ),
3273                        vec![source_range],
3274                    )))
3275                }
3276            }
3277        }
3278        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3279            format!(
3280                "The `{}` function accepts either a line Segment or a list of points",
3281                kind.function_name()
3282            ),
3283            vec![source_range],
3284        ))),
3285    }
3286}
3287
3288async fn axis_constraint(
3289    kind: AxisConstraintKind,
3290    exec_state: &mut ExecState,
3291    args: Args,
3292) -> Result<KclValue, KclError> {
3293    let input: KclValue =
3294        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
3295
3296    // User could pass in a single line, or a sequence of points.
3297    match input {
3298        KclValue::Segment { value } => {
3299            // Single-line case.
3300            axis_constraint_line(value, kind, exec_state, args)
3301        }
3302        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
3303            // Sequence of points case.
3304            axis_constraint_points(value, kind, exec_state, args)
3305        }
3306        other => Err(KclError::new_semantic(KclErrorDetails::new(
3307            format!(
3308                "{}() accepts either a line Segment or a list of at least two points, but you provided {}",
3309                kind.function_name(),
3310                other.human_friendly_type(),
3311            ),
3312            vec![args.source_range],
3313        ))),
3314    }
3315}
3316
3317/// User has provided a single line to align along the given axis.
3318fn axis_constraint_line(
3319    segment: Box<AbstractSegment>,
3320    kind: AxisConstraintKind,
3321    exec_state: &mut ExecState,
3322    args: Args,
3323) -> Result<KclValue, KclError> {
3324    let line = extract_axis_line_vars(&segment, kind, args.source_range)?;
3325    let range = args.source_range;
3326    let solver_p0 = DatumPoint::new_xy(
3327        line.start[0].to_constraint_id(range)?,
3328        line.start[1].to_constraint_id(range)?,
3329    );
3330    let solver_p1 = DatumPoint::new_xy(
3331        line.end[0].to_constraint_id(range)?,
3332        line.end[1].to_constraint_id(range)?,
3333    );
3334    let solver_line = DatumLineSegment::new(solver_p0, solver_p1);
3335    let constraint = kind.line_constraint(solver_line);
3336    #[cfg(feature = "artifact-graph")]
3337    let constraint_id = exec_state.next_object_id();
3338    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3339        return Err(KclError::new_semantic(KclErrorDetails::new(
3340            format!("{}() can only be used inside a sketch block", kind.function_name()),
3341            vec![args.source_range],
3342        )));
3343    };
3344    sketch_state.solver_constraints.push(constraint);
3345    #[cfg(feature = "artifact-graph")]
3346    {
3347        let constraint = kind.line_artifact_constraint(line.object_id);
3348        sketch_state.sketch_constraints.push(constraint_id);
3349        track_constraint(constraint_id, constraint, exec_state, &args);
3350    }
3351    Ok(KclValue::none())
3352}
3353
3354/// User has provided a sequence of points to align along the given axis.
3355fn axis_constraint_points(
3356    point_values: Vec<KclValue>,
3357    kind: AxisConstraintKind,
3358    exec_state: &mut ExecState,
3359    args: Args,
3360) -> Result<KclValue, KclError> {
3361    if point_values.len() < 2 {
3362        return Err(KclError::new_semantic(KclErrorDetails::new(
3363            format!("{}() point list must contain at least two points", kind.function_name()),
3364            vec![args.source_range],
3365        )));
3366    }
3367
3368    #[cfg(feature = "artifact-graph")]
3369    let trackable_point_ids = point_values
3370        .iter()
3371        .map(|point| match point {
3372            KclValue::Segment { value: segment } => {
3373                let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3374                    return None;
3375                };
3376                let UnsolvedSegmentKind::Point { .. } = &unsolved.kind else {
3377                    return None;
3378                };
3379                Some(ConstraintSegment::from(unsolved.object_id))
3380            }
3381            point if point2d_is_origin(point) => Some(ConstraintSegment::ORIGIN),
3382            _ => None,
3383        })
3384        .collect::<Option<Vec<_>>>();
3385
3386    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3387        return Err(KclError::new_semantic(KclErrorDetails::new(
3388            format!("{}() can only be used inside a sketch block", kind.function_name()),
3389            vec![args.source_range],
3390        )));
3391    };
3392
3393    let points: Vec<PointToAlign> = point_values
3394        .iter()
3395        .map(|point| extract_axis_point_vars(point, kind, args.source_range))
3396        .collect::<Result<_, _>>()?;
3397
3398    let mut solver_constraints = Vec::with_capacity(points.len().saturating_sub(1));
3399
3400    let mut var_points = Vec::new();
3401    let mut fix_points = Vec::new();
3402    for point in points {
3403        match point {
3404            PointToAlign::Variable { x, y } => var_points.push((x, y)),
3405            PointToAlign::Fixed { x, y } => fix_points.push((x, y)),
3406        }
3407    }
3408    if fix_points.len() > 1 {
3409        return Err(KclError::new_semantic(KclErrorDetails::new(
3410            format!(
3411                "{}() point list can contain at most 1 fixed point, but you provided {}",
3412                kind.function_name(),
3413                fix_points.len()
3414            ),
3415            vec![args.source_range],
3416        )));
3417    }
3418
3419    if let Some(fix_point) = fix_points.pop() {
3420        // We have to align all the variable points with this singular fixed point.
3421        // For points 0, 1, 2, ..., n, create constraints
3422        // fixed(0.x, fix.x)
3423        // fixed(1.x, fix.x)
3424        // ...
3425        // fixed(n.x, fix.x)
3426        // (or y, whatever is appropriate)
3427        for point in var_points {
3428            let solver_point = datum_point([point.0, point.1], args.source_range)?;
3429            let fix_point_mm = (fix_point.0.to_mm(), fix_point.1.to_mm());
3430            solver_constraints.push(kind.constraint_aligning_point_to_constant(solver_point, fix_point_mm));
3431        }
3432    } else {
3433        // For points 0, 1, 2, ..., n, create constraints
3434        // vertical(0, 1)
3435        // vertical(0, 2)
3436        // ...
3437        // vertical(0, n)
3438        // (or horizontal, if appropriate)
3439        let mut points = var_points.into_iter();
3440        let first_point = points.next().ok_or_else(|| {
3441            KclError::new_semantic(KclErrorDetails::new(
3442                format!("{}() point list must contain at least two points", kind.function_name()),
3443                vec![args.source_range],
3444            ))
3445        })?;
3446        let anchor = datum_point([first_point.0, first_point.1], args.source_range)?;
3447        for point in points {
3448            let solver_point = datum_point([point.0, point.1], args.source_range)?;
3449            solver_constraints.push(kind.point_pair_constraint(anchor, solver_point));
3450        }
3451    }
3452    sketch_state.solver_constraints.extend(solver_constraints);
3453
3454    #[cfg(feature = "artifact-graph")]
3455    if let Some(point_ids) = trackable_point_ids {
3456        let constraint_id = exec_state.next_object_id();
3457        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3458            debug_assert!(false, "Constraint created outside a sketch block");
3459            return Ok(KclValue::none());
3460        };
3461        sketch_state.sketch_constraints.push(constraint_id);
3462        let constraint = kind.point_artifact_constraint(point_ids);
3463        track_constraint(constraint_id, constraint, exec_state, &args);
3464    }
3465
3466    Ok(KclValue::none())
3467}
3468
3469pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3470    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3471        "lines",
3472        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3473        exec_state,
3474    )?;
3475    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
3476        KclError::new_semantic(KclErrorDetails::new(
3477            "must have two input lines".to_owned(),
3478            vec![args.source_range],
3479        ))
3480    })?;
3481    let KclValue::Segment { value: segment0 } = &line0 else {
3482        return Err(KclError::new_semantic(KclErrorDetails::new(
3483            "line argument must be a Segment".to_owned(),
3484            vec![args.source_range],
3485        )));
3486    };
3487    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
3488        return Err(KclError::new_internal(KclErrorDetails::new(
3489            "line must be an unsolved Segment".to_owned(),
3490            vec![args.source_range],
3491        )));
3492    };
3493    let UnsolvedSegmentKind::Line {
3494        start: start0,
3495        end: end0,
3496        ..
3497    } = &unsolved0.kind
3498    else {
3499        return Err(KclError::new_semantic(KclErrorDetails::new(
3500            "line argument must be a line, no other type of Segment".to_owned(),
3501            vec![args.source_range],
3502        )));
3503    };
3504    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
3505        return Err(KclError::new_semantic(KclErrorDetails::new(
3506            "line's start x coordinate must be a var".to_owned(),
3507            vec![args.source_range],
3508        )));
3509    };
3510    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
3511        return Err(KclError::new_semantic(KclErrorDetails::new(
3512            "line's start y coordinate must be a var".to_owned(),
3513            vec![args.source_range],
3514        )));
3515    };
3516    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
3517        return Err(KclError::new_semantic(KclErrorDetails::new(
3518            "line's end x coordinate must be a var".to_owned(),
3519            vec![args.source_range],
3520        )));
3521    };
3522    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
3523        return Err(KclError::new_semantic(KclErrorDetails::new(
3524            "line's end y coordinate must be a var".to_owned(),
3525            vec![args.source_range],
3526        )));
3527    };
3528    let KclValue::Segment { value: segment1 } = &line1 else {
3529        return Err(KclError::new_semantic(KclErrorDetails::new(
3530            "line argument must be a Segment".to_owned(),
3531            vec![args.source_range],
3532        )));
3533    };
3534    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
3535        return Err(KclError::new_internal(KclErrorDetails::new(
3536            "line must be an unsolved Segment".to_owned(),
3537            vec![args.source_range],
3538        )));
3539    };
3540    let UnsolvedSegmentKind::Line {
3541        start: start1,
3542        end: end1,
3543        ..
3544    } = &unsolved1.kind
3545    else {
3546        return Err(KclError::new_semantic(KclErrorDetails::new(
3547            "line argument must be a line, no other type of Segment".to_owned(),
3548            vec![args.source_range],
3549        )));
3550    };
3551    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
3552        return Err(KclError::new_semantic(KclErrorDetails::new(
3553            "line's start x coordinate must be a var".to_owned(),
3554            vec![args.source_range],
3555        )));
3556    };
3557    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
3558        return Err(KclError::new_semantic(KclErrorDetails::new(
3559            "line's start y coordinate must be a var".to_owned(),
3560            vec![args.source_range],
3561        )));
3562    };
3563    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
3564        return Err(KclError::new_semantic(KclErrorDetails::new(
3565            "line's end x coordinate must be a var".to_owned(),
3566            vec![args.source_range],
3567        )));
3568    };
3569    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3570        return Err(KclError::new_semantic(KclErrorDetails::new(
3571            "line's end y coordinate must be a var".to_owned(),
3572            vec![args.source_range],
3573        )));
3574    };
3575
3576    // All coordinates are sketch vars. Proceed.
3577    let sketch_constraint = SketchConstraint {
3578        kind: SketchConstraintKind::Angle {
3579            line0: crate::execution::ConstrainableLine2d {
3580                object_id: unsolved0.object_id,
3581                vars: [
3582                    crate::front::Point2d {
3583                        x: *line0_p0_x,
3584                        y: *line0_p0_y,
3585                    },
3586                    crate::front::Point2d {
3587                        x: *line0_p1_x,
3588                        y: *line0_p1_y,
3589                    },
3590                ],
3591            },
3592            line1: crate::execution::ConstrainableLine2d {
3593                object_id: unsolved1.object_id,
3594                vars: [
3595                    crate::front::Point2d {
3596                        x: *line1_p0_x,
3597                        y: *line1_p0_y,
3598                    },
3599                    crate::front::Point2d {
3600                        x: *line1_p1_x,
3601                        y: *line1_p1_y,
3602                    },
3603                ],
3604            },
3605        },
3606        meta: vec![args.source_range.into()],
3607    };
3608    Ok(KclValue::SketchConstraint {
3609        value: Box::new(sketch_constraint),
3610    })
3611}
3612
3613async fn lines_at_angle(
3614    angle_kind: LinesAtAngleKind,
3615    exec_state: &mut ExecState,
3616    args: Args,
3617) -> Result<KclValue, KclError> {
3618    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3619        "lines",
3620        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3621        exec_state,
3622    )?;
3623    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
3624        KclError::new_semantic(KclErrorDetails::new(
3625            "must have two input lines".to_owned(),
3626            vec![args.source_range],
3627        ))
3628    })?;
3629
3630    let KclValue::Segment { value: segment0 } = &line0 else {
3631        return Err(KclError::new_semantic(KclErrorDetails::new(
3632            "line argument must be a Segment".to_owned(),
3633            vec![args.source_range],
3634        )));
3635    };
3636    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
3637        return Err(KclError::new_internal(KclErrorDetails::new(
3638            "line must be an unsolved Segment".to_owned(),
3639            vec![args.source_range],
3640        )));
3641    };
3642    let UnsolvedSegmentKind::Line {
3643        start: start0,
3644        end: end0,
3645        ..
3646    } = &unsolved0.kind
3647    else {
3648        return Err(KclError::new_semantic(KclErrorDetails::new(
3649            "line argument must be a line, no other type of Segment".to_owned(),
3650            vec![args.source_range],
3651        )));
3652    };
3653    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
3654        return Err(KclError::new_semantic(KclErrorDetails::new(
3655            "line's start x coordinate must be a var".to_owned(),
3656            vec![args.source_range],
3657        )));
3658    };
3659    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
3660        return Err(KclError::new_semantic(KclErrorDetails::new(
3661            "line's start y coordinate must be a var".to_owned(),
3662            vec![args.source_range],
3663        )));
3664    };
3665    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
3666        return Err(KclError::new_semantic(KclErrorDetails::new(
3667            "line's end x coordinate must be a var".to_owned(),
3668            vec![args.source_range],
3669        )));
3670    };
3671    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
3672        return Err(KclError::new_semantic(KclErrorDetails::new(
3673            "line's end y coordinate must be a var".to_owned(),
3674            vec![args.source_range],
3675        )));
3676    };
3677    let KclValue::Segment { value: segment1 } = &line1 else {
3678        return Err(KclError::new_semantic(KclErrorDetails::new(
3679            "line argument must be a Segment".to_owned(),
3680            vec![args.source_range],
3681        )));
3682    };
3683    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
3684        return Err(KclError::new_internal(KclErrorDetails::new(
3685            "line must be an unsolved Segment".to_owned(),
3686            vec![args.source_range],
3687        )));
3688    };
3689    let UnsolvedSegmentKind::Line {
3690        start: start1,
3691        end: end1,
3692        ..
3693    } = &unsolved1.kind
3694    else {
3695        return Err(KclError::new_semantic(KclErrorDetails::new(
3696            "line argument must be a line, no other type of Segment".to_owned(),
3697            vec![args.source_range],
3698        )));
3699    };
3700    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
3701        return Err(KclError::new_semantic(KclErrorDetails::new(
3702            "line's start x coordinate must be a var".to_owned(),
3703            vec![args.source_range],
3704        )));
3705    };
3706    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
3707        return Err(KclError::new_semantic(KclErrorDetails::new(
3708            "line's start y coordinate must be a var".to_owned(),
3709            vec![args.source_range],
3710        )));
3711    };
3712    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
3713        return Err(KclError::new_semantic(KclErrorDetails::new(
3714            "line's end x coordinate must be a var".to_owned(),
3715            vec![args.source_range],
3716        )));
3717    };
3718    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3719        return Err(KclError::new_semantic(KclErrorDetails::new(
3720            "line's end y coordinate must be a var".to_owned(),
3721            vec![args.source_range],
3722        )));
3723    };
3724
3725    let range = args.source_range;
3726    let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3727        line0_p0_x.to_constraint_id(range)?,
3728        line0_p0_y.to_constraint_id(range)?,
3729    );
3730    let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3731        line0_p1_x.to_constraint_id(range)?,
3732        line0_p1_y.to_constraint_id(range)?,
3733    );
3734    let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
3735    let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3736        line1_p0_x.to_constraint_id(range)?,
3737        line1_p0_y.to_constraint_id(range)?,
3738    );
3739    let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3740        line1_p1_x.to_constraint_id(range)?,
3741        line1_p1_y.to_constraint_id(range)?,
3742    );
3743    let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
3744    let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
3745    #[cfg(feature = "artifact-graph")]
3746    let constraint_id = exec_state.next_object_id();
3747    // Save the constraint to be used for solving.
3748    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3749        return Err(KclError::new_semantic(KclErrorDetails::new(
3750            format!(
3751                "{}() can only be used inside a sketch block",
3752                angle_kind.to_function_name()
3753            ),
3754            vec![args.source_range],
3755        )));
3756    };
3757    sketch_state.solver_constraints.push(constraint);
3758    #[cfg(feature = "artifact-graph")]
3759    {
3760        let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
3761        sketch_state.sketch_constraints.push(constraint_id);
3762        track_constraint(constraint_id, constraint, exec_state, &args);
3763    }
3764    Ok(KclValue::none())
3765}
3766
3767pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3768    axis_constraint(AxisConstraintKind::Horizontal, exec_state, args).await
3769}
3770
3771pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3772    axis_constraint(AxisConstraintKind::Vertical, exec_state, args).await
3773}