Skip to main content

kcl_lib/std/
constraints.rs

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