Skip to main content

kcl_lib/std/
constraints.rs

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