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