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