Skip to main content

kcl_lib/std/
constraints.rs

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