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