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    let optional_constraints = {
1426        let placeholder_control_ids = control_object_ids
1427            .iter()
1428            .map(|control_object_id| {
1429                exec_state.add_placeholder_scene_object(*control_object_id, args.source_range, args.node_path.clone())
1430            })
1431            .collect::<Vec<_>>();
1432        control_polygon_edge_object_ids.iter().for_each(|edge_object_id| {
1433            exec_state.add_placeholder_scene_object(*edge_object_id, args.source_range, args.node_path.clone());
1434        });
1435        let spline_object_id =
1436            exec_state.add_placeholder_scene_object(spline_object_id, args.source_range, args.node_path.clone());
1437
1438        let mut optional_constraints = Vec::new();
1439        for (index, [x_value, y_value]) in control_values.iter().enumerate() {
1440            let control_object_id = placeholder_control_ids[index];
1441            if !(exec_state.segment_ids_edited_contains(&control_object_id)
1442                || exec_state.segment_ids_edited_contains(&spline_object_id))
1443            {
1444                continue;
1445            }
1446
1447            if let Some(x_var) = x_value.as_sketch_var() {
1448                let x_initial_value = x_var.initial_value_to_solver_units(
1449                    exec_state,
1450                    args.source_range,
1451                    "edited segment fixed constraint value",
1452                )?;
1453                optional_constraints.push(SolverConstraint::Fixed(
1454                    x_var.id.to_constraint_id(args.source_range)?,
1455                    x_initial_value.n,
1456                ));
1457            }
1458
1459            if let Some(y_var) = y_value.as_sketch_var() {
1460                let y_initial_value = y_var.initial_value_to_solver_units(
1461                    exec_state,
1462                    args.source_range,
1463                    "edited segment fixed constraint value",
1464                )?;
1465                optional_constraints.push(SolverConstraint::Fixed(
1466                    y_var.id.to_constraint_id(args.source_range)?,
1467                    y_initial_value.n,
1468                ));
1469            }
1470        }
1471        optional_constraints
1472    };
1473
1474    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1475        return Err(KclError::new_semantic(KclErrorDetails::new(
1476            "controlPointSpline() can only be used inside a sketch block".to_owned(),
1477            vec![args.source_range],
1478        )));
1479    };
1480    sketch_state.needed_by_engine.push(segment.clone());
1481
1482    sketch_state.solver_optional_constraints.extend(optional_constraints);
1483
1484    let meta = segment.meta.clone();
1485    let abstract_segment = AbstractSegment {
1486        repr: SegmentRepr::Unsolved {
1487            segment: Box::new(segment),
1488        },
1489        meta,
1490    };
1491    Ok(KclValue::Segment {
1492        value: Box::new(abstract_segment),
1493    })
1494}
1495
1496pub async fn coincident(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1497    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1498        "points",
1499        &RuntimeType::Array(
1500            Box::new(RuntimeType::Union(vec![RuntimeType::segment(), RuntimeType::point2d()])),
1501            ArrayLen::Minimum(2),
1502        ),
1503        exec_state,
1504    )?;
1505    if points.len() > 2 {
1506        return coincident_points(points, exec_state, args);
1507    }
1508    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
1509        KclError::new_semantic(KclErrorDetails::new(
1510            "must have two input points".to_owned(),
1511            vec![args.source_range],
1512        ))
1513    })?;
1514
1515    let range = args.source_range;
1516    match (&point0, &point1) {
1517        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1518            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1519                return Err(KclError::new_semantic(KclErrorDetails::new(
1520                    "first point must be an unsolved segment".to_owned(),
1521                    vec![args.source_range],
1522                )));
1523            };
1524            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1525                return Err(KclError::new_semantic(KclErrorDetails::new(
1526                    "second point must be an unsolved segment".to_owned(),
1527                    vec![args.source_range],
1528                )));
1529            };
1530            match (&unsolved0.kind, &unsolved1.kind) {
1531                (
1532                    UnsolvedSegmentKind::Point { position: pos0, .. },
1533                    UnsolvedSegmentKind::Point { position: pos1, .. },
1534                ) => {
1535                    let p0_x = &pos0[0];
1536                    let p0_y = &pos0[1];
1537                    match (p0_x, p0_y) {
1538                        (UnsolvedExpr::Unknown(p0_x), UnsolvedExpr::Unknown(p0_y)) => {
1539                            let p1_x = &pos1[0];
1540                            let p1_y = &pos1[1];
1541                            match (p1_x, p1_y) {
1542                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1543                                    let constraint = SolverConstraint::PointsCoincident(
1544                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
1545                                            p0_x.to_constraint_id(range)?,
1546                                            p0_y.to_constraint_id(range)?,
1547                                        ),
1548                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
1549                                            p1_x.to_constraint_id(range)?,
1550                                            p1_y.to_constraint_id(range)?,
1551                                        ),
1552                                    );
1553                                    let constraint_id = exec_state.next_object_id();
1554                                    // Save the constraint to be used for solving.
1555                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1556                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1557                                            "coincident() can only be used inside a sketch block".to_owned(),
1558                                            vec![args.source_range],
1559                                        )));
1560                                    };
1561                                    sketch_state.solver_constraints.push(constraint);
1562                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1563                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1564                                    });
1565                                    sketch_state.sketch_constraints.push(constraint_id);
1566                                    track_constraint(constraint_id, constraint, exec_state, &args);
1567                                    Ok(KclValue::none())
1568                                }
1569                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1570                                    let p1_x = KclValue::Number {
1571                                        value: p1_x.n,
1572                                        ty: p1_x.ty,
1573                                        meta: vec![args.source_range.into()],
1574                                    };
1575                                    let p1_y = KclValue::Number {
1576                                        value: p1_y.n,
1577                                        ty: p1_y.ty,
1578                                        meta: vec![args.source_range.into()],
1579                                    };
1580                                    let (constraint_x, constraint_y) =
1581                                        coincident_constraints_fixed(*p0_x, *p0_y, &p1_x, &p1_y, exec_state, &args)?;
1582
1583                                    let constraint_id = exec_state.next_object_id();
1584                                    // Save the constraint to be used for solving.
1585                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1586                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1587                                            "coincident() can only be used inside a sketch block".to_owned(),
1588                                            vec![args.source_range],
1589                                        )));
1590                                    };
1591                                    sketch_state.solver_constraints.push(constraint_x);
1592                                    sketch_state.solver_constraints.push(constraint_y);
1593                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1594                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1595                                    });
1596                                    sketch_state.sketch_constraints.push(constraint_id);
1597                                    track_constraint(constraint_id, constraint, exec_state, &args);
1598                                    Ok(KclValue::none())
1599                                }
1600                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1601                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1602                                    // TODO: sketch-api: unimplemented
1603                                    Err(KclError::new_semantic(KclErrorDetails::new(
1604                                        "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(),
1605                                        vec![args.source_range],
1606                                    )))
1607                                }
1608                            }
1609                        }
1610                        (UnsolvedExpr::Known(p0_x), UnsolvedExpr::Known(p0_y)) => {
1611                            let p1_x = &pos1[0];
1612                            let p1_y = &pos1[1];
1613                            match (p1_x, p1_y) {
1614                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
1615                                    let p0_x = KclValue::Number {
1616                                        value: p0_x.n,
1617                                        ty: p0_x.ty,
1618                                        meta: vec![args.source_range.into()],
1619                                    };
1620                                    let p0_y = KclValue::Number {
1621                                        value: p0_y.n,
1622                                        ty: p0_y.ty,
1623                                        meta: vec![args.source_range.into()],
1624                                    };
1625                                    let (constraint_x, constraint_y) =
1626                                        coincident_constraints_fixed(*p1_x, *p1_y, &p0_x, &p0_y, exec_state, &args)?;
1627
1628                                    let constraint_id = exec_state.next_object_id();
1629                                    // Save the constraint to be used for solving.
1630                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1631                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1632                                            "coincident() can only be used inside a sketch block".to_owned(),
1633                                            vec![args.source_range],
1634                                        )));
1635                                    };
1636                                    sketch_state.solver_constraints.push(constraint_x);
1637                                    sketch_state.solver_constraints.push(constraint_y);
1638                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1639                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1640                                    });
1641                                    sketch_state.sketch_constraints.push(constraint_id);
1642                                    track_constraint(constraint_id, constraint, exec_state, &args);
1643                                    Ok(KclValue::none())
1644                                }
1645                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1646                                    if *p0_x != *p1_x || *p0_y != *p1_y {
1647                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1648                                            "Coincident constraint between two fixed points failed since coordinates differ"
1649                                                .to_owned(),
1650                                            vec![args.source_range],
1651                                        )));
1652                                    }
1653                                    Ok(KclValue::none())
1654                                }
1655                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1656                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1657                                    // TODO: sketch-api: unimplemented
1658                                    Err(KclError::new_semantic(KclErrorDetails::new(
1659                                        "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(),
1660                                        vec![args.source_range],
1661                                    )))
1662                                }
1663                            }
1664                        }
1665                        (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1666                        | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1667                            // The segment is a point with one sketch var.
1668                            Err(KclError::new_semantic(KclErrorDetails::new(
1669                                "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(),
1670                                vec![args.source_range],
1671                            )))
1672                        }
1673                    }
1674                }
1675                // Point-Line or Line-Point case: create perpendicular distance constraint with distance 0
1676                (
1677                    UnsolvedSegmentKind::Point {
1678                        position: point_pos, ..
1679                    },
1680                    UnsolvedSegmentKind::Line {
1681                        start: line_start,
1682                        end: line_end,
1683                        ..
1684                    },
1685                )
1686                | (
1687                    UnsolvedSegmentKind::Line {
1688                        start: line_start,
1689                        end: line_end,
1690                        ..
1691                    },
1692                    UnsolvedSegmentKind::Point {
1693                        position: point_pos, ..
1694                    },
1695                ) => {
1696                    let point_x = &point_pos[0];
1697                    let point_y = &point_pos[1];
1698                    match (point_x, point_y) {
1699                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1700                            // Extract line start and end coordinates
1701                            let (start_x, start_y) = (&line_start[0], &line_start[1]);
1702                            let (end_x, end_y) = (&line_end[0], &line_end[1]);
1703
1704                            match (start_x, start_y, end_x, end_y) {
1705                                (
1706                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1707                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1708                                ) => {
1709                                    let point = DatumPoint::new_xy(
1710                                        point_x.to_constraint_id(range)?,
1711                                        point_y.to_constraint_id(range)?,
1712                                    );
1713                                    let line_segment = DatumLineSegment::new(
1714                                        DatumPoint::new_xy(sx.to_constraint_id(range)?, sy.to_constraint_id(range)?),
1715                                        DatumPoint::new_xy(ex.to_constraint_id(range)?, ey.to_constraint_id(range)?),
1716                                    );
1717                                    let constraint = SolverConstraint::PointLineDistance(point, line_segment, 0.0);
1718
1719                                    let constraint_id = exec_state.next_object_id();
1720
1721                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1722                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1723                                            "coincident() can only be used inside a sketch block".to_owned(),
1724                                            vec![args.source_range],
1725                                        )));
1726                                    };
1727                                    sketch_state.solver_constraints.push(constraint);
1728                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1729                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1730                                    });
1731                                    sketch_state.sketch_constraints.push(constraint_id);
1732                                    track_constraint(constraint_id, constraint, exec_state, &args);
1733                                    Ok(KclValue::none())
1734                                }
1735                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1736                                    "Line segment endpoints must be sketch variables for point-segment coincident constraint".to_owned(),
1737                                    vec![args.source_range],
1738                                ))),
1739                            }
1740                        }
1741                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1742                            "Point coordinates must be sketch variables for point-segment coincident constraint"
1743                                .to_owned(),
1744                            vec![args.source_range],
1745                        ))),
1746                    }
1747                }
1748                // Point-Arc or Arc-Point case: create PointArcCoincident constraint
1749                (
1750                    UnsolvedSegmentKind::Point {
1751                        position: point_pos, ..
1752                    },
1753                    UnsolvedSegmentKind::Arc {
1754                        start: arc_start,
1755                        end: arc_end,
1756                        center: arc_center,
1757                        ..
1758                    },
1759                )
1760                | (
1761                    UnsolvedSegmentKind::Arc {
1762                        start: arc_start,
1763                        end: arc_end,
1764                        center: arc_center,
1765                        ..
1766                    },
1767                    UnsolvedSegmentKind::Point {
1768                        position: point_pos, ..
1769                    },
1770                ) => {
1771                    let point_x = &point_pos[0];
1772                    let point_y = &point_pos[1];
1773                    match (point_x, point_y) {
1774                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1775                            // Extract arc center, start, and end coordinates
1776                            let (center_x, center_y) = (&arc_center[0], &arc_center[1]);
1777                            let (start_x, start_y) = (&arc_start[0], &arc_start[1]);
1778                            let (end_x, end_y) = (&arc_end[0], &arc_end[1]);
1779
1780                            match (center_x, center_y, start_x, start_y, end_x, end_y) {
1781                                (
1782                                    UnsolvedExpr::Unknown(cx), UnsolvedExpr::Unknown(cy),
1783                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1784                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1785                                ) => {
1786                                    let point = DatumPoint::new_xy(
1787                                        point_x.to_constraint_id(range)?,
1788                                        point_y.to_constraint_id(range)?,
1789                                    );
1790                                    let circular_arc = DatumCircularArc {
1791                                        center: DatumPoint::new_xy(
1792                                            cx.to_constraint_id(range)?,
1793                                            cy.to_constraint_id(range)?,
1794                                        ),
1795                                        start: DatumPoint::new_xy(
1796                                            sx.to_constraint_id(range)?,
1797                                            sy.to_constraint_id(range)?,
1798                                        ),
1799                                        end: DatumPoint::new_xy(
1800                                            ex.to_constraint_id(range)?,
1801                                            ey.to_constraint_id(range)?,
1802                                        ),
1803                                    };
1804                                    let constraint = SolverConstraint::PointArcCoincident(circular_arc, point);
1805
1806                                    let constraint_id = exec_state.next_object_id();
1807
1808                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1809                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1810                                            "coincident() can only be used inside a sketch block".to_owned(),
1811                                            vec![args.source_range],
1812                                        )));
1813                                    };
1814                                    sketch_state.solver_constraints.push(constraint);
1815                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1816                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1817                                    });
1818                                    sketch_state.sketch_constraints.push(constraint_id);
1819                                    track_constraint(constraint_id, constraint, exec_state, &args);
1820                                    Ok(KclValue::none())
1821                                }
1822                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1823                                    "Arc center, start, and end points must be sketch variables for point-arc coincident constraint".to_owned(),
1824                                    vec![args.source_range],
1825                                ))),
1826                            }
1827                        }
1828                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1829                            "Point coordinates must be sketch variables for point-arc coincident constraint".to_owned(),
1830                            vec![args.source_range],
1831                        ))),
1832                    }
1833                }
1834                // Point-Circle or Circle-Point case: constrain point-to-center distance
1835                // to equal the circle radius.
1836                (
1837                    UnsolvedSegmentKind::Point {
1838                        position: point_pos, ..
1839                    },
1840                    UnsolvedSegmentKind::Circle {
1841                        start: circle_start,
1842                        center: circle_center,
1843                        ..
1844                    },
1845                )
1846                | (
1847                    UnsolvedSegmentKind::Circle {
1848                        start: circle_start,
1849                        center: circle_center,
1850                        ..
1851                    },
1852                    UnsolvedSegmentKind::Point {
1853                        position: point_pos, ..
1854                    },
1855                ) => {
1856                    let point_x = &point_pos[0];
1857                    let point_y = &point_pos[1];
1858                    match (point_x, point_y) {
1859                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1860                            // Extract circle center and start coordinates.
1861                            let (center_x, center_y) = (&circle_center[0], &circle_center[1]);
1862                            let (start_x, start_y) = (&circle_start[0], &circle_start[1]);
1863
1864                            match (center_x, center_y, start_x, start_y) {
1865                                (
1866                                    UnsolvedExpr::Unknown(cx),
1867                                    UnsolvedExpr::Unknown(cy),
1868                                    UnsolvedExpr::Unknown(sx),
1869                                    UnsolvedExpr::Unknown(sy),
1870                                ) => {
1871                                    let point_radius_line = DatumLineSegment::new(
1872                                        DatumPoint::new_xy(
1873                                            cx.to_constraint_id(range)?,
1874                                            cy.to_constraint_id(range)?,
1875                                        ),
1876                                        DatumPoint::new_xy(
1877                                            point_x.to_constraint_id(range)?,
1878                                            point_y.to_constraint_id(range)?,
1879                                        ),
1880                                    );
1881                                    let circle_radius_line = DatumLineSegment::new(
1882                                        DatumPoint::new_xy(
1883                                            cx.to_constraint_id(range)?,
1884                                            cy.to_constraint_id(range)?,
1885                                        ),
1886                                        DatumPoint::new_xy(
1887                                            sx.to_constraint_id(range)?,
1888                                            sy.to_constraint_id(range)?,
1889                                        ),
1890                                    );
1891                                    let constraint =
1892                                        SolverConstraint::LinesEqualLength(point_radius_line, circle_radius_line);
1893
1894                                    let constraint_id = exec_state.next_object_id();
1895
1896                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1897                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1898                                            "coincident() can only be used inside a sketch block".to_owned(),
1899                                            vec![args.source_range],
1900                                        )));
1901                                    };
1902                                    sketch_state.solver_constraints.push(constraint);
1903                                    let constraint = crate::front::Constraint::Coincident(Coincident {
1904                                        segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1905                                    });
1906                                    sketch_state.sketch_constraints.push(constraint_id);
1907                                    track_constraint(constraint_id, constraint, exec_state, &args);
1908                                    Ok(KclValue::none())
1909                                }
1910                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1911                                    "Circle start and center points must be sketch variables for point-circle coincident constraint".to_owned(),
1912                                    vec![args.source_range],
1913                                ))),
1914                            }
1915                        }
1916                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1917                            "Point coordinates must be sketch variables for point-circle coincident constraint"
1918                                .to_owned(),
1919                            vec![args.source_range],
1920                        ))),
1921                    }
1922                }
1923                // Line-Line case: create parallel constraint and perpendicular distance of zero
1924                (
1925                    UnsolvedSegmentKind::Line {
1926                        start: line0_start,
1927                        end: line0_end,
1928                        ..
1929                    },
1930                    UnsolvedSegmentKind::Line {
1931                        start: line1_start,
1932                        end: line1_end,
1933                        ..
1934                    },
1935                ) => {
1936                    // Extract line coordinates
1937                    let (line0_start_x, line0_start_y) = (&line0_start[0], &line0_start[1]);
1938                    let (line0_end_x, line0_end_y) = (&line0_end[0], &line0_end[1]);
1939                    let (line1_start_x, line1_start_y) = (&line1_start[0], &line1_start[1]);
1940                    let (line1_end_x, line1_end_y) = (&line1_end[0], &line1_end[1]);
1941
1942                    match (
1943                        line0_start_x,
1944                        line0_start_y,
1945                        line0_end_x,
1946                        line0_end_y,
1947                        line1_start_x,
1948                        line1_start_y,
1949                        line1_end_x,
1950                        line1_end_y,
1951                    ) {
1952                        (
1953                            UnsolvedExpr::Unknown(l0_sx),
1954                            UnsolvedExpr::Unknown(l0_sy),
1955                            UnsolvedExpr::Unknown(l0_ex),
1956                            UnsolvedExpr::Unknown(l0_ey),
1957                            UnsolvedExpr::Unknown(l1_sx),
1958                            UnsolvedExpr::Unknown(l1_sy),
1959                            UnsolvedExpr::Unknown(l1_ex),
1960                            UnsolvedExpr::Unknown(l1_ey),
1961                        ) => {
1962                            // Create line segments for the solver
1963                            let line0_segment = DatumLineSegment::new(
1964                                DatumPoint::new_xy(l0_sx.to_constraint_id(range)?, l0_sy.to_constraint_id(range)?),
1965                                DatumPoint::new_xy(l0_ex.to_constraint_id(range)?, l0_ey.to_constraint_id(range)?),
1966                            );
1967                            let line1_segment = DatumLineSegment::new(
1968                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?),
1969                                DatumPoint::new_xy(l1_ex.to_constraint_id(range)?, l1_ey.to_constraint_id(range)?),
1970                            );
1971
1972                            // Create parallel constraint
1973                            let parallel_constraint =
1974                                SolverConstraint::LinesAtAngle(line0_segment, line1_segment, AngleKind::Parallel);
1975
1976                            // Create perpendicular distance constraint from first line to start point of second line
1977                            let point_on_line1 =
1978                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?);
1979                            let distance_constraint =
1980                                SolverConstraint::PointLineDistance(point_on_line1, line0_segment, 0.0);
1981
1982                            let constraint_id = exec_state.next_object_id();
1983
1984                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1985                                return Err(KclError::new_semantic(KclErrorDetails::new(
1986                                    "coincident() can only be used inside a sketch block".to_owned(),
1987                                    vec![args.source_range],
1988                                )));
1989                            };
1990                            // Push both constraints to achieve collinearity
1991                            sketch_state.solver_constraints.push(parallel_constraint);
1992                            sketch_state.solver_constraints.push(distance_constraint);
1993                            let constraint = crate::front::Constraint::Coincident(Coincident {
1994                                segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1995                            });
1996                            sketch_state.sketch_constraints.push(constraint_id);
1997                            track_constraint(constraint_id, constraint, exec_state, &args);
1998                            Ok(KclValue::none())
1999                        }
2000                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2001                            "Line segment endpoints must be sketch variables for line-line coincident constraint"
2002                                .to_owned(),
2003                            vec![args.source_range],
2004                        ))),
2005                    }
2006                }
2007                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2008                    format!(
2009                        "coincident supports point-point, point-segment, or segment-segment; found {:?} and {:?}",
2010                        &unsolved0.kind, &unsolved1.kind
2011                    ),
2012                    vec![args.source_range],
2013                ))),
2014            }
2015        }
2016        // One argument is a Segment and the other is a Point2d literal.
2017        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2018        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2019            let Some(pt) = <[TyF64; 2]>::from_kcl_val(point2d) else {
2020                return Err(KclError::new_semantic(KclErrorDetails::new(
2021                    "Expected a Segment or Point2d (e.g. [1mm, 2mm])".to_owned(),
2022                    vec![args.source_range],
2023                )));
2024            };
2025            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2026                return Err(KclError::new_semantic(KclErrorDetails::new(
2027                    "segment must be an unsolved segment".to_owned(),
2028                    vec![args.source_range],
2029                )));
2030            };
2031            match &unsolved.kind {
2032                UnsolvedSegmentKind::Point { position, .. } => {
2033                    let p_x = &position[0];
2034                    let p_y = &position[1];
2035                    match (p_x, p_y) {
2036                        (UnsolvedExpr::Unknown(p_x), UnsolvedExpr::Unknown(p_y)) => {
2037                            let pt_x = KclValue::Number {
2038                                value: pt[0].n,
2039                                ty: pt[0].ty,
2040                                meta: vec![args.source_range.into()],
2041                            };
2042                            let pt_y = KclValue::Number {
2043                                value: pt[1].n,
2044                                ty: pt[1].ty,
2045                                meta: vec![args.source_range.into()],
2046                            };
2047                            let (constraint_x, constraint_y) =
2048                                coincident_constraints_fixed(*p_x, *p_y, &pt_x, &pt_y, exec_state, &args)?;
2049
2050                            let constraint_id = exec_state.next_object_id();
2051                            let coincident_segments = coincident_segments_for_segment_and_point2d(
2052                                unsolved.object_id,
2053                                point2d,
2054                                matches!((&point0, &point1), (KclValue::Segment { .. }, _)),
2055                            );
2056                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2057                                return Err(KclError::new_semantic(KclErrorDetails::new(
2058                                    "coincident() can only be used inside a sketch block".to_owned(),
2059                                    vec![args.source_range],
2060                                )));
2061                            };
2062                            sketch_state.solver_constraints.push(constraint_x);
2063                            sketch_state.solver_constraints.push(constraint_y);
2064                            let constraint = crate::front::Constraint::Coincident(Coincident {
2065                                segments: coincident_segments,
2066                            });
2067                            sketch_state.sketch_constraints.push(constraint_id);
2068                            track_constraint(constraint_id, constraint, exec_state, &args);
2069                            Ok(KclValue::none())
2070                        }
2071                        (UnsolvedExpr::Known(known_x), UnsolvedExpr::Known(known_y)) => {
2072                            let pt_x_val = normalize_to_solver_distance_unit(
2073                                &KclValue::Number {
2074                                    value: pt[0].n,
2075                                    ty: pt[0].ty,
2076                                    meta: vec![args.source_range.into()],
2077                                },
2078                                args.source_range,
2079                                exec_state,
2080                                "coincident constraint value",
2081                            )?;
2082                            let pt_y_val = normalize_to_solver_distance_unit(
2083                                &KclValue::Number {
2084                                    value: pt[1].n,
2085                                    ty: pt[1].ty,
2086                                    meta: vec![args.source_range.into()],
2087                                },
2088                                args.source_range,
2089                                exec_state,
2090                                "coincident constraint value",
2091                            )?;
2092                            let Some(pt_x) = pt_x_val.as_ty_f64() else {
2093                                return Err(KclError::new_semantic(KclErrorDetails::new(
2094                                    "Expected number for Point2d x coordinate".to_owned(),
2095                                    vec![args.source_range],
2096                                )));
2097                            };
2098                            let Some(pt_y) = pt_y_val.as_ty_f64() else {
2099                                return Err(KclError::new_semantic(KclErrorDetails::new(
2100                                    "Expected number for Point2d y coordinate".to_owned(),
2101                                    vec![args.source_range],
2102                                )));
2103                            };
2104                            let known_x_val = normalize_to_solver_distance_unit(
2105                                &KclValue::Number {
2106                                    value: known_x.n,
2107                                    ty: known_x.ty,
2108                                    meta: vec![args.source_range.into()],
2109                                },
2110                                args.source_range,
2111                                exec_state,
2112                                "coincident constraint value",
2113                            )?;
2114                            let Some(known_x_f) = known_x_val.as_ty_f64() else {
2115                                return Err(KclError::new_semantic(KclErrorDetails::new(
2116                                    "Expected number for known x coordinate".to_owned(),
2117                                    vec![args.source_range],
2118                                )));
2119                            };
2120                            let known_y_val = normalize_to_solver_distance_unit(
2121                                &KclValue::Number {
2122                                    value: known_y.n,
2123                                    ty: known_y.ty,
2124                                    meta: vec![args.source_range.into()],
2125                                },
2126                                args.source_range,
2127                                exec_state,
2128                                "coincident constraint value",
2129                            )?;
2130                            let Some(known_y_f) = known_y_val.as_ty_f64() else {
2131                                return Err(KclError::new_semantic(KclErrorDetails::new(
2132                                    "Expected number for known y coordinate".to_owned(),
2133                                    vec![args.source_range],
2134                                )));
2135                            };
2136                            if known_x_f.n != pt_x.n || known_y_f.n != pt_y.n {
2137                                return Err(KclError::new_semantic(KclErrorDetails::new(
2138                                    "Coincident constraint between two fixed points failed since coordinates differ"
2139                                        .to_owned(),
2140                                    vec![args.source_range],
2141                                )));
2142                            }
2143                            Ok(KclValue::none())
2144                        }
2145                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2146                            "Point coordinates must have consistent known/unknown status for coincident constraint"
2147                                .to_owned(),
2148                            vec![args.source_range],
2149                        ))),
2150                    }
2151                }
2152                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2153                    "A Point2d can only be constrained coincident with a point segment, not a line or arc".to_owned(),
2154                    vec![args.source_range],
2155                ))),
2156            }
2157        }
2158        // Both arguments are Point2d literals -- just verify equality.
2159        _ => {
2160            let pt0 = <[TyF64; 2]>::from_kcl_val(&point0);
2161            let pt1 = <[TyF64; 2]>::from_kcl_val(&point1);
2162            match (pt0, pt1) {
2163                (Some(a), Some(b)) => {
2164                    // Normalize both to solver units and compare.
2165                    let a_x = normalize_to_solver_distance_unit(
2166                        &KclValue::Number {
2167                            value: a[0].n,
2168                            ty: a[0].ty,
2169                            meta: vec![args.source_range.into()],
2170                        },
2171                        args.source_range,
2172                        exec_state,
2173                        "coincident constraint value",
2174                    )?;
2175                    let a_y = normalize_to_solver_distance_unit(
2176                        &KclValue::Number {
2177                            value: a[1].n,
2178                            ty: a[1].ty,
2179                            meta: vec![args.source_range.into()],
2180                        },
2181                        args.source_range,
2182                        exec_state,
2183                        "coincident constraint value",
2184                    )?;
2185                    let b_x = normalize_to_solver_distance_unit(
2186                        &KclValue::Number {
2187                            value: b[0].n,
2188                            ty: b[0].ty,
2189                            meta: vec![args.source_range.into()],
2190                        },
2191                        args.source_range,
2192                        exec_state,
2193                        "coincident constraint value",
2194                    )?;
2195                    let b_y = normalize_to_solver_distance_unit(
2196                        &KclValue::Number {
2197                            value: b[1].n,
2198                            ty: b[1].ty,
2199                            meta: vec![args.source_range.into()],
2200                        },
2201                        args.source_range,
2202                        exec_state,
2203                        "coincident constraint value",
2204                    )?;
2205                    if a_x.as_ty_f64().map(|v| v.n) != b_x.as_ty_f64().map(|v| v.n)
2206                        || a_y.as_ty_f64().map(|v| v.n) != b_y.as_ty_f64().map(|v| v.n)
2207                    {
2208                        return Err(KclError::new_semantic(KclErrorDetails::new(
2209                            "Coincident constraint between two fixed points failed since coordinates differ".to_owned(),
2210                            vec![args.source_range],
2211                        )));
2212                    }
2213                    Ok(KclValue::none())
2214                }
2215                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2216                    "All inputs must be Segments or Point2d values".to_owned(),
2217                    vec![args.source_range],
2218                ))),
2219            }
2220        }
2221    }
2222}
2223
2224fn coincident_points(
2225    point_values: Vec<KclValue>,
2226    exec_state: &mut ExecState,
2227    args: Args,
2228) -> Result<KclValue, KclError> {
2229    if point_values.len() < 2 {
2230        return Err(KclError::new_semantic(KclErrorDetails::new(
2231            "coincident() point list must contain at least two points".to_owned(),
2232            vec![args.source_range],
2233        )));
2234    }
2235
2236    // For every point return either a fixed point or a variable point
2237    let points = point_values
2238        .iter()
2239        .map(|point| extract_multi_coincident_point(point, args.source_range))
2240        .collect::<Result<Vec<_>, _>>()?;
2241
2242    let constraint_segments = points.iter().map(|point| point.constraint_segment).collect::<Vec<_>>();
2243
2244    let mut variable_points = Vec::new();
2245    let mut fixed_points = Vec::new();
2246    for point in points {
2247        match point.point {
2248            PointToAlign::Variable { x, y } => variable_points.push([x, y]),
2249            PointToAlign::Fixed { x, y } => fixed_points.push([x, y]),
2250        }
2251    }
2252
2253    let mut solver_constraints = Vec::with_capacity(point_values.len().saturating_sub(1) * 2);
2254    if let Some((anchor_fixed, remaining_fixed_points)) = fixed_points.split_first() {
2255        // A fixed point becomes the shared target location for every variable point.
2256        if remaining_fixed_points
2257            .iter()
2258            .any(|point| !fixed_points_match(point, anchor_fixed))
2259        {
2260            return Err(KclError::new_semantic(KclErrorDetails::new(
2261                "coincident() with more than two inputs can include at most one fixed point location".to_owned(),
2262                vec![args.source_range],
2263            )));
2264        }
2265
2266        let anchor_x = ty_f64_to_kcl_value(anchor_fixed[0].clone(), args.source_range);
2267        let anchor_y = ty_f64_to_kcl_value(anchor_fixed[1].clone(), args.source_range);
2268        for point in variable_points {
2269            let (constraint_x, constraint_y) =
2270                coincident_constraints_fixed(point[0], point[1], &anchor_x, &anchor_y, exec_state, &args)?;
2271            solver_constraints.push(constraint_x);
2272            solver_constraints.push(constraint_y);
2273        }
2274    } else {
2275        // With only variable points, anchor everything to the first point.
2276        let mut points = variable_points.into_iter();
2277        let first_point = points.next().ok_or_else(|| {
2278            KclError::new_semantic(KclErrorDetails::new(
2279                "coincident() point list must contain at least two points".to_owned(),
2280                vec![args.source_range],
2281            ))
2282        })?;
2283        let anchor = datum_point(first_point, args.source_range)?;
2284        for point in points {
2285            let solver_point = datum_point(point, args.source_range)?;
2286            solver_constraints.push(SolverConstraint::PointsCoincident(anchor, solver_point));
2287        }
2288    }
2289
2290    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2291        return Err(KclError::new_semantic(KclErrorDetails::new(
2292            "coincident() can only be used inside a sketch block".to_owned(),
2293            vec![args.source_range],
2294        )));
2295    };
2296    sketch_state.solver_constraints.extend(solver_constraints);
2297
2298    // Keep one artifact-graph coincident constraint even though the solver sees multiple relations.
2299    let constraint_id = exec_state.next_object_id();
2300    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2301        debug_assert!(false, "Constraint created outside a sketch block");
2302        return Ok(KclValue::none());
2303    };
2304    sketch_state.sketch_constraints.push(constraint_id);
2305    let constraint = Constraint::Coincident(Coincident {
2306        segments: constraint_segments,
2307    });
2308    track_constraint(constraint_id, constraint, exec_state, &args);
2309
2310    Ok(KclValue::none())
2311}
2312
2313fn extract_multi_coincident_point(
2314    input: &KclValue,
2315    source_range: crate::SourceRange,
2316) -> Result<CoincidentPointInput, KclError> {
2317    // Normalize each multi-input item into either a fixed point or solver-backed point vars.
2318    match input {
2319        KclValue::Segment { value: segment } => {
2320            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2321                return Err(KclError::new_semantic(KclErrorDetails::new(
2322                    "coincident() with more than two inputs only supports unsolved points or ORIGIN".to_owned(),
2323                    vec![source_range],
2324                )));
2325            };
2326            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2327                return Err(KclError::new_semantic(KclErrorDetails::new(
2328                    format!(
2329                        "coincident() with more than two inputs only supports points or ORIGIN, but one item is {}",
2330                        unsolved.kind.human_friendly_kind_with_article()
2331                    ),
2332                    vec![source_range],
2333                )));
2334            };
2335            match (&position[0], &position[1]) {
2336                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(CoincidentPointInput {
2337                    point: PointToAlign::Fixed {
2338                        x: x.to_owned(),
2339                        y: y.to_owned(),
2340                    },
2341                    constraint_segment: unsolved.object_id.into(),
2342                }),
2343                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(CoincidentPointInput {
2344                    point: PointToAlign::Variable { x: *x, y: *y },
2345                    constraint_segment: unsolved.object_id.into(),
2346                }),
2347                // Mixed points not supported
2348                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..))
2349                | (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => Err(KclError::new_semantic(
2350                    KclErrorDetails::new(
2351                        "coincident() with more than two inputs requires each point to be fully fixed or fully variable"
2352                            .to_owned(),
2353                        vec![source_range],
2354                    ),
2355                )),
2356            }
2357        }
2358        point if point2d_is_origin(point) => {
2359            let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point) else {
2360                debug_assert!(false, "Origin literal should coerce to Point2d");
2361                return Err(KclError::new_internal(KclErrorDetails::new(
2362                    "Origin literal could not be converted to a point".to_owned(),
2363                    vec![source_range],
2364                )));
2365            };
2366            Ok(CoincidentPointInput {
2367                point: PointToAlign::Fixed { x, y },
2368                constraint_segment: ConstraintSegment::ORIGIN,
2369            })
2370        }
2371        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2372            "coincident() with more than two inputs only supports points and ORIGIN".to_owned(),
2373            vec![source_range],
2374        ))),
2375    }
2376}
2377
2378#[derive(Debug, Clone)]
2379struct CoincidentPointInput {
2380    point: PointToAlign,
2381    constraint_segment: ConstraintSegment,
2382}
2383
2384fn fixed_points_match(a: &[TyF64; 2], b: &[TyF64; 2]) -> bool {
2385    a[0].to_mm() == b[0].to_mm() && a[1].to_mm() == b[1].to_mm()
2386}
2387
2388fn ty_f64_to_kcl_value(value: TyF64, source_range: crate::SourceRange) -> KclValue {
2389    KclValue::Number {
2390        value: value.n,
2391        ty: value.ty,
2392        meta: vec![source_range.into()],
2393    }
2394}
2395
2396fn track_constraint(constraint_id: ObjectId, constraint: Constraint, exec_state: &mut ExecState, args: &Args) {
2397    let sketch_id = {
2398        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2399            debug_assert!(false, "Constraint created outside a sketch block");
2400            return;
2401        };
2402        sketch_state.sketch_id
2403    };
2404    let Some(sketch_id) = sketch_id else {
2405        debug_assert!(false, "Constraint created without a sketch id");
2406        return;
2407    };
2408    let artifact_id = exec_state.next_artifact_id();
2409    exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
2410        id: artifact_id,
2411        sketch_id,
2412        constraint_id,
2413        constraint_type: SketchBlockConstraintType::from(&constraint),
2414        code_ref: CodeRef::placeholder(args.source_range),
2415    }));
2416    exec_state.add_scene_object(
2417        Object {
2418            id: constraint_id,
2419            kind: ObjectKind::Constraint { constraint },
2420            label: Default::default(),
2421            comments: Default::default(),
2422            artifact_id,
2423            source: SourceRef::new(args.source_range, args.node_path.clone()),
2424        },
2425        args.source_range,
2426    );
2427}
2428
2429/// Order of points has been erased when calling this function.
2430fn coincident_constraints_fixed(
2431    p0_x: SketchVarId,
2432    p0_y: SketchVarId,
2433    p1_x: &KclValue,
2434    p1_y: &KclValue,
2435    exec_state: &mut ExecState,
2436    args: &Args,
2437) -> Result<(ezpz::Constraint, ezpz::Constraint), KclError> {
2438    let p1_x_number_value =
2439        normalize_to_solver_distance_unit(p1_x, p1_x.into(), exec_state, "coincident constraint value")?;
2440    let p1_y_number_value =
2441        normalize_to_solver_distance_unit(p1_y, p1_y.into(), exec_state, "coincident constraint value")?;
2442    let Some(p1_x) = p1_x_number_value.as_ty_f64() else {
2443        let message = format!(
2444            "Expected number after coercion, but found {}",
2445            p1_x_number_value.human_friendly_type()
2446        );
2447        debug_assert!(false, "{}", &message);
2448        return Err(KclError::new_internal(KclErrorDetails::new(
2449            message,
2450            vec![args.source_range],
2451        )));
2452    };
2453    let Some(p1_y) = p1_y_number_value.as_ty_f64() else {
2454        let message = format!(
2455            "Expected number after coercion, but found {}",
2456            p1_y_number_value.human_friendly_type()
2457        );
2458        debug_assert!(false, "{}", &message);
2459        return Err(KclError::new_internal(KclErrorDetails::new(
2460            message,
2461            vec![args.source_range],
2462        )));
2463    };
2464    let constraint_x = SolverConstraint::Fixed(p0_x.to_constraint_id(args.source_range)?, p1_x.n);
2465    let constraint_y = SolverConstraint::Fixed(p0_y.to_constraint_id(args.source_range)?, p1_y.n);
2466    Ok((constraint_x, constraint_y))
2467}
2468
2469pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2470    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2471        "points",
2472        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2473        exec_state,
2474    )?;
2475    let label_position = get_constraint_label_position(exec_state, &args, "distance")?;
2476    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
2477        KclError::new_semantic(KclErrorDetails::new(
2478            "must have two input points".to_owned(),
2479            vec![args.source_range],
2480        ))
2481    })?;
2482
2483    match (&point0, &point1) {
2484        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2485            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2486                return Err(KclError::new_semantic(KclErrorDetails::new(
2487                    "first point must be an unsolved segment".to_owned(),
2488                    vec![args.source_range],
2489                )));
2490            };
2491            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2492                return Err(KclError::new_semantic(KclErrorDetails::new(
2493                    "second point must be an unsolved segment".to_owned(),
2494                    vec![args.source_range],
2495                )));
2496            };
2497            match (&unsolved0.kind, &unsolved1.kind) {
2498                (
2499                    UnsolvedSegmentKind::Point { position: pos0, .. },
2500                    UnsolvedSegmentKind::Point { position: pos1, .. },
2501                ) => {
2502                    // Both segments are points. Create a distance constraint
2503                    // between them.
2504                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2505                        (
2506                            UnsolvedExpr::Unknown(p0_x),
2507                            UnsolvedExpr::Unknown(p0_y),
2508                            UnsolvedExpr::Unknown(p1_x),
2509                            UnsolvedExpr::Unknown(p1_y),
2510                        ) => {
2511                            // All coordinates are sketch vars. Proceed.
2512                            let sketch_constraint = SketchConstraint {
2513                                kind: SketchConstraintKind::Distance {
2514                                    points: [
2515                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2516                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2517                                            object_id: unsolved0.object_id,
2518                                        }),
2519                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2520                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2521                                            object_id: unsolved1.object_id,
2522                                        }),
2523                                    ],
2524                                    label_position,
2525                                },
2526                                meta: vec![args.source_range.into()],
2527                            };
2528                            Ok(KclValue::SketchConstraint {
2529                                value: Box::new(sketch_constraint),
2530                            })
2531                        }
2532                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2533                            "unimplemented: distance() arguments must be all sketch vars in all coordinates".to_owned(),
2534                            vec![args.source_range],
2535                        ))),
2536                    }
2537                }
2538                (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Line { .. })
2539                | (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Point { .. }) => {
2540                    let (point_segment, line_segment) = match (&unsolved0.kind, &unsolved1.kind) {
2541                        (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Line { .. }) => (unsolved0, unsolved1),
2542                        (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Point { .. }) => (unsolved1, unsolved0),
2543                        _ => {
2544                            return Err(KclError::new_semantic(KclErrorDetails::new(
2545                                "distance() expected a point-line segment pair".to_owned(),
2546                                vec![args.source_range],
2547                            )));
2548                        }
2549                    };
2550                    let point =
2551                        constrainable_point_from_unsolved_segment(point_segment, "distance", args.source_range)?;
2552                    let line = constrainable_line_from_unsolved_segment(line_segment, "distance", args.source_range)?;
2553
2554                    Ok(KclValue::SketchConstraint {
2555                        value: Box::new(SketchConstraint {
2556                            kind: SketchConstraintKind::PointLineDistance {
2557                                point: ConstrainablePoint2dOrOrigin::Point(point),
2558                                line,
2559                                input_object_ids: [Some(unsolved0.object_id), Some(unsolved1.object_id)],
2560                                label_position,
2561                            },
2562                            meta: vec![args.source_range.into()],
2563                        }),
2564                    })
2565                }
2566                (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Arc { .. })
2567                | (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Circle { .. })
2568                | (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Point { .. })
2569                | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Point { .. }) => {
2570                    let (point_segment, circular_segment) = match (&unsolved0.kind, &unsolved1.kind) {
2571                        (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Arc { .. })
2572                        | (UnsolvedSegmentKind::Point { .. }, UnsolvedSegmentKind::Circle { .. }) => {
2573                            (unsolved0, unsolved1)
2574                        }
2575                        (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Point { .. })
2576                        | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Point { .. }) => {
2577                            (unsolved1, unsolved0)
2578                        }
2579                        _ => {
2580                            return Err(KclError::new_semantic(KclErrorDetails::new(
2581                                "distance() expected a point-arc or point-circle segment pair".to_owned(),
2582                                vec![args.source_range],
2583                            )));
2584                        }
2585                    };
2586                    let point =
2587                        constrainable_point_from_unsolved_segment(point_segment, "distance", args.source_range)?;
2588                    let (center, start, end) =
2589                        constrainable_circular_from_unsolved_segment(circular_segment, "distance", args.source_range)?;
2590
2591                    Ok(KclValue::SketchConstraint {
2592                        value: Box::new(SketchConstraint {
2593                            kind: SketchConstraintKind::PointCircularDistance {
2594                                point: ConstrainablePoint2dOrOrigin::Point(point),
2595                                center,
2596                                start,
2597                                end,
2598                                input_object_ids: [Some(unsolved0.object_id), Some(unsolved1.object_id)],
2599                                label_position,
2600                            },
2601                            meta: vec![args.source_range.into()],
2602                        }),
2603                    })
2604                }
2605                (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Arc { .. })
2606                | (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Circle { .. })
2607                | (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Line { .. })
2608                | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Line { .. }) => {
2609                    let (line_segment, circular_segment) = match (&unsolved0.kind, &unsolved1.kind) {
2610                        (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Arc { .. })
2611                        | (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Circle { .. }) => {
2612                            (unsolved0, unsolved1)
2613                        }
2614                        (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Line { .. })
2615                        | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Line { .. }) => {
2616                            (unsolved1, unsolved0)
2617                        }
2618                        _ => {
2619                            return Err(KclError::new_semantic(KclErrorDetails::new(
2620                                "distance() expected a line-arc or line-circle segment pair".to_owned(),
2621                                vec![args.source_range],
2622                            )));
2623                        }
2624                    };
2625                    let line = constrainable_line_from_unsolved_segment(line_segment, "distance", args.source_range)?;
2626                    let (center, start, end) =
2627                        constrainable_circular_from_unsolved_segment(circular_segment, "distance", args.source_range)?;
2628
2629                    Ok(KclValue::SketchConstraint {
2630                        value: Box::new(SketchConstraint {
2631                            kind: SketchConstraintKind::LineCircularDistance {
2632                                line,
2633                                center,
2634                                start,
2635                                end,
2636                                input_object_ids: [unsolved0.object_id, unsolved1.object_id],
2637                                label_position,
2638                            },
2639                            meta: vec![args.source_range.into()],
2640                        }),
2641                    })
2642                }
2643                (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Arc { .. })
2644                | (UnsolvedSegmentKind::Arc { .. }, UnsolvedSegmentKind::Circle { .. })
2645                | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Arc { .. })
2646                | (UnsolvedSegmentKind::Circle { .. }, UnsolvedSegmentKind::Circle { .. }) => {
2647                    let (center0, start0, end0) =
2648                        constrainable_circular_from_unsolved_segment(unsolved0, "distance", args.source_range)?;
2649                    let (center1, start1, end1) =
2650                        constrainable_circular_from_unsolved_segment(unsolved1, "distance", args.source_range)?;
2651
2652                    Ok(KclValue::SketchConstraint {
2653                        value: Box::new(SketchConstraint {
2654                            kind: SketchConstraintKind::CircularCircularDistance {
2655                                center0,
2656                                start0,
2657                                end0,
2658                                center1,
2659                                start1,
2660                                end1,
2661                                input_object_ids: [unsolved0.object_id, unsolved1.object_id],
2662                                label_position,
2663                            },
2664                            meta: vec![args.source_range.into()],
2665                        }),
2666                    })
2667                }
2668                (UnsolvedSegmentKind::Line { .. }, UnsolvedSegmentKind::Line { .. }) => {
2669                    let line0 = constrainable_line_from_unsolved_segment(unsolved0, "distance", args.source_range)?;
2670                    let line1 = constrainable_line_from_unsolved_segment(unsolved1, "distance", args.source_range)?;
2671
2672                    Ok(KclValue::SketchConstraint {
2673                        value: Box::new(SketchConstraint {
2674                            kind: SketchConstraintKind::LineLineDistance {
2675                                line0,
2676                                line1,
2677                                input_object_ids: [unsolved0.object_id, unsolved1.object_id],
2678                                label_position,
2679                            },
2680                            meta: vec![args.source_range.into()],
2681                        }),
2682                    })
2683                }
2684                (UnsolvedSegmentKind::ControlPointSpline { .. }, _)
2685                | (_, UnsolvedSegmentKind::ControlPointSpline { .. }) => {
2686                    Err(KclError::new_semantic(KclErrorDetails::new(
2687                        "distance() does not yet support control point spline segments".to_owned(),
2688                        vec![args.source_range],
2689                    )))
2690                }
2691            }
2692        }
2693        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2694        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2695            if !point2d_is_origin(point2d) {
2696                return Err(KclError::new_semantic(KclErrorDetails::new(
2697                    "distance() Point2d arguments must be ORIGIN".to_owned(),
2698                    vec![args.source_range],
2699                )));
2700            }
2701
2702            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2703                return Err(KclError::new_semantic(KclErrorDetails::new(
2704                    "segment must be an unsolved segment".to_owned(),
2705                    vec![args.source_range],
2706                )));
2707            };
2708            let segment_first = matches!((&point0, &point1), (KclValue::Segment { .. }, _));
2709            let input_object_ids = if segment_first {
2710                [Some(unsolved.object_id), None]
2711            } else {
2712                [None, Some(unsolved.object_id)]
2713            };
2714            match &unsolved.kind {
2715                UnsolvedSegmentKind::Point { position, .. } => match (&position[0], &position[1]) {
2716                    (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2717                        let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2718                            vars: crate::front::Point2d {
2719                                x: *point_x,
2720                                y: *point_y,
2721                            },
2722                            object_id: unsolved.object_id,
2723                        });
2724                        let points = if segment_first {
2725                            [point, ConstrainablePoint2dOrOrigin::Origin]
2726                        } else {
2727                            [ConstrainablePoint2dOrOrigin::Origin, point]
2728                        };
2729                        Ok(KclValue::SketchConstraint {
2730                            value: Box::new(SketchConstraint {
2731                                kind: SketchConstraintKind::Distance { points, label_position },
2732                                meta: vec![args.source_range.into()],
2733                            }),
2734                        })
2735                    }
2736                    _ => Err(KclError::new_semantic(KclErrorDetails::new(
2737                        "unimplemented: distance() point arguments must be sketch vars in all coordinates".to_owned(),
2738                        vec![args.source_range],
2739                    ))),
2740                },
2741                UnsolvedSegmentKind::Line { .. } => {
2742                    let line = constrainable_line_from_unsolved_segment(unsolved, "distance", args.source_range)?;
2743                    Ok(KclValue::SketchConstraint {
2744                        value: Box::new(SketchConstraint {
2745                            kind: SketchConstraintKind::PointLineDistance {
2746                                point: ConstrainablePoint2dOrOrigin::Origin,
2747                                line,
2748                                input_object_ids,
2749                                label_position,
2750                            },
2751                            meta: vec![args.source_range.into()],
2752                        }),
2753                    })
2754                }
2755                UnsolvedSegmentKind::Arc { .. } | UnsolvedSegmentKind::Circle { .. } => {
2756                    let (center, start, end) =
2757                        constrainable_circular_from_unsolved_segment(unsolved, "distance", args.source_range)?;
2758                    Ok(KclValue::SketchConstraint {
2759                        value: Box::new(SketchConstraint {
2760                            kind: SketchConstraintKind::PointCircularDistance {
2761                                point: ConstrainablePoint2dOrOrigin::Origin,
2762                                center,
2763                                start,
2764                                end,
2765                                input_object_ids,
2766                                label_position,
2767                            },
2768                            meta: vec![args.source_range.into()],
2769                        }),
2770                    })
2771                }
2772                UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_semantic(KclErrorDetails::new(
2773                    "distance() does not yet support control point spline segments".to_owned(),
2774                    vec![args.source_range],
2775                ))),
2776            }
2777        }
2778        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2779            "distance() arguments must be point segments or ORIGIN".to_owned(),
2780            vec![args.source_range],
2781        ))),
2782    }
2783}
2784
2785fn get_constraint_label_position(
2786    exec_state: &mut ExecState,
2787    args: &Args,
2788    constraint_name: &str,
2789) -> Result<Option<Point2d<Number>>, KclError> {
2790    let label_position = args.get_kw_arg_opt::<[TyF64; 2]>("labelPosition", &RuntimeType::point2d(), exec_state)?;
2791
2792    label_position
2793        .map(|label| {
2794            TyF64::to_point2d(&label).map_err(|_| {
2795                KclError::new_internal(KclErrorDetails::new(
2796                    format!("Could not convert {constraint_name} label position to a Point2d"),
2797                    vec![args.source_range],
2798                ))
2799            })
2800        })
2801        .transpose()
2802}
2803
2804/// Helper function to create a radius or diameter constraint from a circular segment.
2805/// Used by both radius() and diameter() functions.
2806fn create_circular_radius_constraint(
2807    segment: KclValue,
2808    constraint_kind: impl Fn([ConstrainablePoint2d; 2]) -> SketchConstraintKind,
2809    source_range: crate::SourceRange,
2810) -> Result<SketchConstraint, KclError> {
2811    // Create a dummy constraint to get its name for error messages
2812    let dummy_constraint = constraint_kind([
2813        ConstrainablePoint2d {
2814            vars: crate::front::Point2d {
2815                x: SketchVarId(0),
2816                y: SketchVarId(0),
2817            },
2818            object_id: ObjectId(0),
2819        },
2820        ConstrainablePoint2d {
2821            vars: crate::front::Point2d {
2822                x: SketchVarId(0),
2823                y: SketchVarId(0),
2824            },
2825            object_id: ObjectId(0),
2826        },
2827    ]);
2828    let function_name = dummy_constraint.name();
2829
2830    let KclValue::Segment { value: seg } = segment else {
2831        return Err(KclError::new_semantic(KclErrorDetails::new(
2832            format!("{}() argument must be a segment", function_name),
2833            vec![source_range],
2834        )));
2835    };
2836    let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2837        return Err(KclError::new_semantic(KclErrorDetails::new(
2838            "segment must be unsolved".to_owned(),
2839            vec![source_range],
2840        )));
2841    };
2842    match &unsolved.kind {
2843        UnsolvedSegmentKind::Arc {
2844            center,
2845            start,
2846            center_object_id,
2847            start_object_id,
2848            ..
2849        }
2850        | UnsolvedSegmentKind::Circle {
2851            center,
2852            start,
2853            center_object_id,
2854            start_object_id,
2855            ..
2856        } => {
2857            // Extract center and start point coordinates
2858            match (&center[0], &center[1], &start[0], &start[1]) {
2859                (
2860                    UnsolvedExpr::Unknown(center_x),
2861                    UnsolvedExpr::Unknown(center_y),
2862                    UnsolvedExpr::Unknown(start_x),
2863                    UnsolvedExpr::Unknown(start_y),
2864                ) => {
2865                    // All coordinates are sketch vars. Create constraint.
2866                    let sketch_constraint = SketchConstraint {
2867                        kind: constraint_kind([
2868                            ConstrainablePoint2d {
2869                                vars: crate::front::Point2d {
2870                                    x: *center_x,
2871                                    y: *center_y,
2872                                },
2873                                object_id: *center_object_id,
2874                            },
2875                            ConstrainablePoint2d {
2876                                vars: crate::front::Point2d {
2877                                    x: *start_x,
2878                                    y: *start_y,
2879                                },
2880                                object_id: *start_object_id,
2881                            },
2882                        ]),
2883                        meta: vec![source_range.into()],
2884                    };
2885                    Ok(sketch_constraint)
2886                }
2887                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2888                    format!(
2889                        "unimplemented: {}() arc or circle segment must have all sketch vars in all coordinates",
2890                        function_name
2891                    ),
2892                    vec![source_range],
2893                ))),
2894            }
2895        }
2896        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2897            format!("{}() argument must be an arc or circle segment", function_name),
2898            vec![source_range],
2899        ))),
2900    }
2901}
2902
2903pub async fn radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2904    let segment: KclValue =
2905        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2906    let label_position = get_constraint_label_position(exec_state, &args, "radius")?;
2907
2908    create_circular_radius_constraint(
2909        segment,
2910        |points| SketchConstraintKind::Radius {
2911            points,
2912            label_position: label_position.clone(),
2913        },
2914        args.source_range,
2915    )
2916    .map(|constraint| KclValue::SketchConstraint {
2917        value: Box::new(constraint),
2918    })
2919}
2920
2921pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2922    let segment: KclValue =
2923        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2924    let label_position = get_constraint_label_position(exec_state, &args, "diameter")?;
2925
2926    create_circular_radius_constraint(
2927        segment,
2928        |points| SketchConstraintKind::Diameter {
2929            points,
2930            label_position: label_position.clone(),
2931        },
2932        args.source_range,
2933    )
2934    .map(|constraint| KclValue::SketchConstraint {
2935        value: Box::new(constraint),
2936    })
2937}
2938
2939pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2940    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2941        "points",
2942        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2943        exec_state,
2944    )?;
2945    let label_position = get_constraint_label_position(exec_state, &args, "horizontalDistance")?;
2946    let [p1, p2] = points.as_slice() else {
2947        return Err(KclError::new_semantic(KclErrorDetails::new(
2948            "must have two input points".to_owned(),
2949            vec![args.source_range],
2950        )));
2951    };
2952    match (p1, p2) {
2953        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2954            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2955                return Err(KclError::new_semantic(KclErrorDetails::new(
2956                    "first point must be an unsolved segment".to_owned(),
2957                    vec![args.source_range],
2958                )));
2959            };
2960            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2961                return Err(KclError::new_semantic(KclErrorDetails::new(
2962                    "second point must be an unsolved segment".to_owned(),
2963                    vec![args.source_range],
2964                )));
2965            };
2966            match (&unsolved0.kind, &unsolved1.kind) {
2967                (
2968                    UnsolvedSegmentKind::Point { position: pos0, .. },
2969                    UnsolvedSegmentKind::Point { position: pos1, .. },
2970                ) => {
2971                    // Both segments are points. Create a horizontal distance constraint
2972                    // between them.
2973                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2974                        (
2975                            UnsolvedExpr::Unknown(p0_x),
2976                            UnsolvedExpr::Unknown(p0_y),
2977                            UnsolvedExpr::Unknown(p1_x),
2978                            UnsolvedExpr::Unknown(p1_y),
2979                        ) => {
2980                            // All coordinates are sketch vars. Proceed.
2981                            let sketch_constraint = SketchConstraint {
2982                                kind: SketchConstraintKind::HorizontalDistance {
2983                                    points: [
2984                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2985                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2986                                            object_id: unsolved0.object_id,
2987                                        }),
2988                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2989                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2990                                            object_id: unsolved1.object_id,
2991                                        }),
2992                                    ],
2993                                    label_position,
2994                                },
2995                                meta: vec![args.source_range.into()],
2996                            };
2997                            Ok(KclValue::SketchConstraint {
2998                                value: Box::new(sketch_constraint),
2999                            })
3000                        }
3001                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3002                            "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
3003                                .to_owned(),
3004                            vec![args.source_range],
3005                        ))),
3006                    }
3007                }
3008                (
3009                    UnsolvedSegmentKind::Point { .. },
3010                    UnsolvedSegmentKind::Line { .. },
3011                )
3012                | (
3013                    UnsolvedSegmentKind::Line { .. },
3014                    UnsolvedSegmentKind::Point { .. },
3015                ) => Err(KclError::new_semantic(KclErrorDetails::new(
3016                    "horizontalDistance() between a point and a line is invalid because the constraint is under-specified".to_owned(),
3017                    vec![args.source_range],
3018                ))),
3019                _ => Err(KclError::new_semantic(KclErrorDetails::new(
3020                    "horizontalDistance() arguments must be unsolved points".to_owned(),
3021                    vec![args.source_range],
3022                ))),
3023            }
3024        }
3025        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
3026        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
3027            if !point2d_is_origin(point2d) {
3028                return Err(KclError::new_semantic(KclErrorDetails::new(
3029                    "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
3030                    vec![args.source_range],
3031                )));
3032            }
3033
3034            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
3035                return Err(KclError::new_semantic(KclErrorDetails::new(
3036                    "segment must be an unsolved segment".to_owned(),
3037                    vec![args.source_range],
3038                )));
3039            };
3040            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
3041                return Err(KclError::new_semantic(KclErrorDetails::new(
3042                    "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
3043                    vec![args.source_range],
3044                )));
3045            };
3046            match (&position[0], &position[1]) {
3047                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
3048                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
3049                        vars: crate::front::Point2d {
3050                            x: *point_x,
3051                            y: *point_y,
3052                        },
3053                        object_id: unsolved.object_id,
3054                    });
3055                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
3056                        [point, ConstrainablePoint2dOrOrigin::Origin]
3057                    } else {
3058                        [ConstrainablePoint2dOrOrigin::Origin, point]
3059                    };
3060                    Ok(KclValue::SketchConstraint {
3061                        value: Box::new(SketchConstraint {
3062                            kind: SketchConstraintKind::HorizontalDistance { points, label_position },
3063                            meta: vec![args.source_range.into()],
3064                        }),
3065                    })
3066                }
3067                _ => Err(KclError::new_semantic(KclErrorDetails::new(
3068                    "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
3069                        .to_owned(),
3070                    vec![args.source_range],
3071                ))),
3072            }
3073        }
3074        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3075            "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
3076            vec![args.source_range],
3077        ))),
3078    }
3079}
3080
3081pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3082    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
3083        "points",
3084        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3085        exec_state,
3086    )?;
3087    let label_position = get_constraint_label_position(exec_state, &args, "verticalDistance")?;
3088    let [p1, p2] = points.as_slice() else {
3089        return Err(KclError::new_semantic(KclErrorDetails::new(
3090            "must have two input points".to_owned(),
3091            vec![args.source_range],
3092        )));
3093    };
3094    match (p1, p2) {
3095        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
3096            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
3097                return Err(KclError::new_semantic(KclErrorDetails::new(
3098                    "first point must be an unsolved segment".to_owned(),
3099                    vec![args.source_range],
3100                )));
3101            };
3102            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
3103                return Err(KclError::new_semantic(KclErrorDetails::new(
3104                    "second point must be an unsolved segment".to_owned(),
3105                    vec![args.source_range],
3106                )));
3107            };
3108            match (&unsolved0.kind, &unsolved1.kind) {
3109                (
3110                    UnsolvedSegmentKind::Point { position: pos0, .. },
3111                    UnsolvedSegmentKind::Point { position: pos1, .. },
3112                ) => {
3113                    // Both segments are points. Create a vertical distance constraint
3114                    // between them.
3115                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
3116                        (
3117                            UnsolvedExpr::Unknown(p0_x),
3118                            UnsolvedExpr::Unknown(p0_y),
3119                            UnsolvedExpr::Unknown(p1_x),
3120                            UnsolvedExpr::Unknown(p1_y),
3121                        ) => {
3122                            // All coordinates are sketch vars. Proceed.
3123                            let sketch_constraint = SketchConstraint {
3124                                kind: SketchConstraintKind::VerticalDistance {
3125                                    points: [
3126                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
3127                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
3128                                            object_id: unsolved0.object_id,
3129                                        }),
3130                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
3131                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
3132                                            object_id: unsolved1.object_id,
3133                                        }),
3134                                    ],
3135                                    label_position,
3136                                },
3137                                meta: vec![args.source_range.into()],
3138                            };
3139                            Ok(KclValue::SketchConstraint {
3140                                value: Box::new(sketch_constraint),
3141                            })
3142                        }
3143                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3144                            "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
3145                                .to_owned(),
3146                            vec![args.source_range],
3147                        ))),
3148                    }
3149                }
3150                (
3151                    UnsolvedSegmentKind::Point { .. },
3152                    UnsolvedSegmentKind::Line { .. },
3153                )
3154                | (
3155                    UnsolvedSegmentKind::Line { .. },
3156                    UnsolvedSegmentKind::Point { .. },
3157                ) => Err(KclError::new_semantic(KclErrorDetails::new(
3158                    "verticalDistance() between a point and a line is invalid because the constraint is under-specified".to_owned(),
3159                    vec![args.source_range],
3160                ))),
3161                _ => Err(KclError::new_semantic(KclErrorDetails::new(
3162                    "verticalDistance() arguments must be unsolved points".to_owned(),
3163                    vec![args.source_range],
3164                ))),
3165            }
3166        }
3167        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
3168            if !point2d_is_origin(point2d) {
3169                return Err(KclError::new_semantic(KclErrorDetails::new(
3170                    "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
3171                    vec![args.source_range],
3172                )));
3173            }
3174
3175            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
3176                return Err(KclError::new_semantic(KclErrorDetails::new(
3177                    "segment must be an unsolved segment".to_owned(),
3178                    vec![args.source_range],
3179                )));
3180            };
3181            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
3182                return Err(KclError::new_semantic(KclErrorDetails::new(
3183                    "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
3184                    vec![args.source_range],
3185                )));
3186            };
3187            match (&position[0], &position[1]) {
3188                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
3189                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
3190                        vars: crate::front::Point2d {
3191                            x: *point_x,
3192                            y: *point_y,
3193                        },
3194                        object_id: unsolved.object_id,
3195                    });
3196                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
3197                        [point, ConstrainablePoint2dOrOrigin::Origin]
3198                    } else {
3199                        [ConstrainablePoint2dOrOrigin::Origin, point]
3200                    };
3201                    Ok(KclValue::SketchConstraint {
3202                        value: Box::new(SketchConstraint {
3203                            kind: SketchConstraintKind::VerticalDistance { points, label_position },
3204                            meta: vec![args.source_range.into()],
3205                        }),
3206                    })
3207                }
3208                _ => Err(KclError::new_semantic(KclErrorDetails::new(
3209                    "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
3210                        .to_owned(),
3211                    vec![args.source_range],
3212                ))),
3213            }
3214        }
3215        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3216            "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
3217            vec![args.source_range],
3218        ))),
3219    }
3220}
3221
3222#[derive(Debug, Clone, Copy)]
3223enum MidpointPointVars {
3224    Segment {
3225        coords: [SketchVarId; 2],
3226        constraint_segment: ConstraintSegment,
3227    },
3228    Origin,
3229}
3230
3231impl MidpointPointVars {
3232    fn constraint_segment(self) -> ConstraintSegment {
3233        match self {
3234            Self::Segment { constraint_segment, .. } => constraint_segment,
3235            Self::Origin => ConstraintSegment::ORIGIN,
3236        }
3237    }
3238}
3239
3240#[derive(Debug, Clone, Copy)]
3241enum MidpointTargetVars {
3242    Line {
3243        start: [SketchVarId; 2],
3244        end: [SketchVarId; 2],
3245        object_id: ObjectId,
3246    },
3247    Arc {
3248        center: [SketchVarId; 2],
3249        start: [SketchVarId; 2],
3250        end: [SketchVarId; 2],
3251        object_id: ObjectId,
3252    },
3253}
3254
3255impl MidpointTargetVars {
3256    fn object_id(self) -> ObjectId {
3257        match self {
3258            Self::Line { object_id, .. } | Self::Arc { object_id, .. } => object_id,
3259        }
3260    }
3261}
3262
3263fn extract_midpoint_point(segment_value: &KclValue, range: crate::SourceRange) -> Result<MidpointPointVars, KclError> {
3264    if point2d_is_origin(segment_value) {
3265        return Ok(MidpointPointVars::Origin);
3266    }
3267
3268    let KclValue::Segment { value: segment } = segment_value else {
3269        return Err(KclError::new_semantic(KclErrorDetails::new(
3270            format!(
3271                "midpoint() point must be a point Segment or ORIGIN, but found {}",
3272                segment_value.human_friendly_type()
3273            ),
3274            vec![range],
3275        )));
3276    };
3277    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3278        return Err(KclError::new_semantic(KclErrorDetails::new(
3279            "midpoint() point must be an unsolved point Segment".to_owned(),
3280            vec![range],
3281        )));
3282    };
3283    let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
3284        return Err(KclError::new_semantic(KclErrorDetails::new(
3285            "midpoint() point must be a point Segment".to_owned(),
3286            vec![range],
3287        )));
3288    };
3289    let (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) = (&position[0], &position[1]) else {
3290        return Err(KclError::new_semantic(KclErrorDetails::new(
3291            "midpoint() point coordinates must be sketch vars".to_owned(),
3292            vec![range],
3293        )));
3294    };
3295
3296    Ok(MidpointPointVars::Segment {
3297        coords: [*point_x, *point_y],
3298        constraint_segment: unsolved.object_id.into(),
3299    })
3300}
3301
3302fn extract_midpoint_target(
3303    segment_value: &KclValue,
3304    range: crate::SourceRange,
3305) -> Result<MidpointTargetVars, KclError> {
3306    let KclValue::Segment { value: segment } = segment_value else {
3307        return Err(KclError::new_semantic(KclErrorDetails::new(
3308            format!(
3309                "midpoint() target must be a line or arc Segment, but found {}",
3310                segment_value.human_friendly_type()
3311            ),
3312            vec![range],
3313        )));
3314    };
3315    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3316        return Err(KclError::new_semantic(KclErrorDetails::new(
3317            "midpoint() target must be an unsolved line or arc Segment".to_owned(),
3318            vec![range],
3319        )));
3320    };
3321    match &unsolved.kind {
3322        UnsolvedSegmentKind::Line { start, end, .. } => {
3323            let (
3324                UnsolvedExpr::Unknown(start_x),
3325                UnsolvedExpr::Unknown(start_y),
3326                UnsolvedExpr::Unknown(end_x),
3327                UnsolvedExpr::Unknown(end_y),
3328            ) = (&start[0], &start[1], &end[0], &end[1])
3329            else {
3330                return Err(KclError::new_semantic(KclErrorDetails::new(
3331                    "midpoint() line coordinates must be sketch vars".to_owned(),
3332                    vec![range],
3333                )));
3334            };
3335
3336            Ok(MidpointTargetVars::Line {
3337                start: [*start_x, *start_y],
3338                end: [*end_x, *end_y],
3339                object_id: unsolved.object_id,
3340            })
3341        }
3342        UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3343            let (
3344                UnsolvedExpr::Unknown(center_x),
3345                UnsolvedExpr::Unknown(center_y),
3346                UnsolvedExpr::Unknown(start_x),
3347                UnsolvedExpr::Unknown(start_y),
3348                UnsolvedExpr::Unknown(end_x),
3349                UnsolvedExpr::Unknown(end_y),
3350            ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3351            else {
3352                return Err(KclError::new_semantic(KclErrorDetails::new(
3353                    "midpoint() arc center/start/end coordinates must be sketch vars".to_owned(),
3354                    vec![range],
3355                )));
3356            };
3357
3358            Ok(MidpointTargetVars::Arc {
3359                center: [*center_x, *center_y],
3360                start: [*start_x, *start_y],
3361                end: [*end_x, *end_y],
3362                object_id: unsolved.object_id,
3363            })
3364        }
3365        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3366            "midpoint() target must be a line or circular arc Segment".to_owned(),
3367            vec![range],
3368        ))),
3369    }
3370}
3371
3372pub async fn midpoint(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3373    let target: KclValue =
3374        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
3375    let point: KclValue = args.get_kw_arg(
3376        "point",
3377        &RuntimeType::Union(vec![RuntimeType::segment(), RuntimeType::point2d()]),
3378        exec_state,
3379    )?;
3380    let range = args.source_range;
3381
3382    let point = extract_midpoint_point(&point, range)?;
3383    let target = extract_midpoint_target(&target, range)?;
3384
3385    let (solver_point, origin_constraints) = match point {
3386        MidpointPointVars::Segment { coords, .. } => (datum_point(coords, range)?, None),
3387        MidpointPointVars::Origin => {
3388            let (origin_point, origin_constraints) = fixed_origin_datum_point(exec_state, range, "midpoint")?;
3389            (origin_point, Some(origin_constraints))
3390        }
3391    };
3392
3393    let constraint_id = exec_state.next_object_id();
3394    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3395        return Err(KclError::new_semantic(KclErrorDetails::new(
3396            "midpoint() can only be used inside a sketch block".to_owned(),
3397            vec![range],
3398        )));
3399    };
3400
3401    if let Some(origin_constraints) = origin_constraints {
3402        sketch_state.solver_constraints.extend(origin_constraints);
3403    }
3404
3405    match target {
3406        MidpointTargetVars::Line { start, end, .. } => {
3407            sketch_state.solver_constraints.push(SolverConstraint::Midpoint(
3408                DatumLineSegment::new(datum_point(start, range)?, datum_point(end, range)?),
3409                solver_point,
3410            ));
3411        }
3412        MidpointTargetVars::Arc { center, start, end, .. } => {
3413            sketch_state
3414                .solver_constraints
3415                .extend(SolverConstraint::point_bisects_arc(
3416                    DatumCircularArc {
3417                        center: datum_point(center, range)?,
3418                        start: datum_point(start, range)?,
3419                        end: datum_point(end, range)?,
3420                    },
3421                    solver_point,
3422                ));
3423        }
3424    }
3425
3426    let constraint = Constraint::Midpoint(Midpoint {
3427        point: point.constraint_segment(),
3428        segment: target.object_id(),
3429    });
3430    sketch_state.sketch_constraints.push(constraint_id);
3431    track_constraint(constraint_id, constraint, exec_state, &args);
3432
3433    Ok(KclValue::none())
3434}
3435
3436pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3437    #[derive(Clone, Copy)]
3438    struct ConstrainableLine {
3439        solver_line: DatumLineSegment,
3440        object_id: ObjectId,
3441    }
3442
3443    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3444        "lines",
3445        &RuntimeType::Array(
3446            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3447            ArrayLen::Minimum(2),
3448        ),
3449        exec_state,
3450    )?;
3451    let range = args.source_range;
3452    let constrainable_lines: Vec<ConstrainableLine> = lines
3453        .iter()
3454        .map(|line| {
3455            let KclValue::Segment { value: segment } = line else {
3456                return Err(KclError::new_semantic(KclErrorDetails::new(
3457                    "line argument must be a Segment".to_owned(),
3458                    vec![args.source_range],
3459                )));
3460            };
3461            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3462                return Err(KclError::new_internal(KclErrorDetails::new(
3463                    "line must be an unsolved Segment".to_owned(),
3464                    vec![args.source_range],
3465                )));
3466            };
3467            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3468                return Err(KclError::new_semantic(KclErrorDetails::new(
3469                    "line argument must be a line, no other type of Segment".to_owned(),
3470                    vec![args.source_range],
3471                )));
3472            };
3473            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
3474                return Err(KclError::new_semantic(KclErrorDetails::new(
3475                    "line's start x coordinate must be a var".to_owned(),
3476                    vec![args.source_range],
3477                )));
3478            };
3479            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
3480                return Err(KclError::new_semantic(KclErrorDetails::new(
3481                    "line's start y coordinate must be a var".to_owned(),
3482                    vec![args.source_range],
3483                )));
3484            };
3485            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
3486                return Err(KclError::new_semantic(KclErrorDetails::new(
3487                    "line's end x coordinate must be a var".to_owned(),
3488                    vec![args.source_range],
3489                )));
3490            };
3491            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
3492                return Err(KclError::new_semantic(KclErrorDetails::new(
3493                    "line's end y coordinate must be a var".to_owned(),
3494                    vec![args.source_range],
3495                )));
3496            };
3497
3498            let solver_line_p0 =
3499                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
3500            let solver_line_p1 =
3501                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
3502
3503            Ok(ConstrainableLine {
3504                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
3505                object_id: unsolved.object_id,
3506            })
3507        })
3508        .collect::<Result<_, _>>()?;
3509
3510    let constraint_id = exec_state.next_object_id();
3511    // Save the constraint to be used for solving.
3512    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3513        return Err(KclError::new_semantic(KclErrorDetails::new(
3514            "equalLength() can only be used inside a sketch block".to_owned(),
3515            vec![args.source_range],
3516        )));
3517    };
3518    let first_line = constrainable_lines[0];
3519    for line in constrainable_lines.iter().skip(1) {
3520        sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
3521            first_line.solver_line,
3522            line.solver_line,
3523        ));
3524    }
3525    let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
3526        lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
3527    });
3528    sketch_state.sketch_constraints.push(constraint_id);
3529    track_constraint(constraint_id, constraint, exec_state, &args);
3530    Ok(KclValue::none())
3531}
3532
3533fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
3534    Ok(DatumPoint::new_xy(
3535        coords[0].to_constraint_id(range)?,
3536        coords[1].to_constraint_id(range)?,
3537    ))
3538}
3539
3540fn sketch_var_initial_value(
3541    sketch_vars: &[KclValue],
3542    id: SketchVarId,
3543    exec_state: &mut ExecState,
3544    range: crate::SourceRange,
3545) -> Result<f64, KclError> {
3546    sketch_vars
3547        .get(id.0)
3548        .and_then(KclValue::as_sketch_var)
3549        .map(|sketch_var| {
3550            sketch_var
3551                .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
3552                .map(|value| value.n)
3553        })
3554        .transpose()?
3555        .ok_or_else(|| {
3556            KclError::new_internal(KclErrorDetails::new(
3557                format!("Missing sketch variable initial value for id {}", id.0),
3558                vec![range],
3559            ))
3560        })
3561}
3562
3563fn radius_guess(
3564    sketch_vars: &[KclValue],
3565    center: [SketchVarId; 2],
3566    point: [SketchVarId; 2],
3567    exec_state: &mut ExecState,
3568    range: crate::SourceRange,
3569) -> Result<f64, KclError> {
3570    let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
3571        - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
3572    let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
3573        - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
3574    Ok(libm::hypot(dx, dy))
3575}
3576
3577fn reflect_point_across_line(point: [f64; 2], axis_start: [f64; 2], axis_end: [f64; 2]) -> [f64; 2] {
3578    let [px, py] = point;
3579    let [ax, ay] = axis_start;
3580    let [bx, by] = axis_end;
3581    let dx = bx - ax;
3582    let dy = by - ay;
3583    let axis_len_sq = dx * dx + dy * dy;
3584    if axis_len_sq <= f64::EPSILON {
3585        return point;
3586    }
3587
3588    let point_from_axis = [px - ax, py - ay];
3589    let projection_scale = (point_from_axis[0] * dx + point_from_axis[1] * dy) / axis_len_sq;
3590    let projected = [ax + projection_scale * dx, ay + projection_scale * dy];
3591
3592    [2.0 * projected[0] - px, 2.0 * projected[1] - py]
3593}
3594
3595/// Calculate some initial guesses for the given points,
3596/// which are being constrained to symmetric across the given line.
3597fn symmetric_hidden_point_guess(
3598    sketch_vars: &[KclValue],
3599    point: [SketchVarId; 2],
3600    axis: SymmetricLineVars,
3601    exec_state: &mut ExecState,
3602    range: crate::SourceRange,
3603) -> Result<[f64; 2], KclError> {
3604    let point = [
3605        sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
3606        sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
3607    ];
3608    let axis_start = [
3609        sketch_var_initial_value(sketch_vars, axis.start[0], exec_state, range)?,
3610        sketch_var_initial_value(sketch_vars, axis.start[1], exec_state, range)?,
3611    ];
3612    let axis_end = [
3613        sketch_var_initial_value(sketch_vars, axis.end[0], exec_state, range)?,
3614        sketch_var_initial_value(sketch_vars, axis.end[1], exec_state, range)?,
3615    ];
3616
3617    Ok(reflect_point_across_line(point, axis_start, axis_end))
3618}
3619
3620fn create_hidden_point(
3621    exec_state: &mut ExecState,
3622    initial_position: [f64; 2],
3623    range: crate::SourceRange,
3624) -> Result<[SketchVarId; 2], KclError> {
3625    let sketch_var_ty = solver_numeric_type(exec_state);
3626    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3627        return Err(KclError::new_semantic(KclErrorDetails::new(
3628            "symmetric() can only be used inside a sketch block".to_owned(),
3629            vec![range],
3630        )));
3631    };
3632
3633    let x_id = sketch_state.next_sketch_var_id();
3634    sketch_state.sketch_vars.push(KclValue::SketchVar {
3635        value: Box::new(crate::execution::SketchVar {
3636            id: x_id,
3637            initial_value: initial_position[0],
3638            ty: sketch_var_ty,
3639            // Synthesized symmetric() support point coord; not source-backed.
3640            node_path: None,
3641            meta: vec![],
3642        }),
3643    });
3644
3645    let y_id = sketch_state.next_sketch_var_id();
3646    sketch_state.sketch_vars.push(KclValue::SketchVar {
3647        value: Box::new(crate::execution::SketchVar {
3648            id: y_id,
3649            initial_value: initial_position[1],
3650            ty: sketch_var_ty,
3651            // Synthesized symmetric() support point coord; not source-backed.
3652            node_path: None,
3653            meta: vec![],
3654        }),
3655    });
3656
3657    Ok([x_id, y_id])
3658}
3659
3660pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3661    #[derive(Debug, Clone, Copy)]
3662    struct RadiusInputVars {
3663        center: [SketchVarId; 2],
3664        start: [SketchVarId; 2],
3665        end: Option<[SketchVarId; 2]>,
3666    }
3667
3668    #[derive(Debug, Clone, Copy)]
3669    enum EqualRadiusInput {
3670        Radius(RadiusInputVars),
3671    }
3672
3673    fn extract_equal_radius_input(
3674        segment_value: &KclValue,
3675        range: crate::SourceRange,
3676    ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
3677        let KclValue::Segment { value: segment } = segment_value else {
3678            return Err(KclError::new_semantic(KclErrorDetails::new(
3679                format!(
3680                    "equalRadius() arguments must be segments but found {}",
3681                    segment_value.human_friendly_type()
3682                ),
3683                vec![range],
3684            )));
3685        };
3686        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3687            return Err(KclError::new_semantic(KclErrorDetails::new(
3688                "equalRadius() arguments must be unsolved segments".to_owned(),
3689                vec![range],
3690            )));
3691        };
3692        match &unsolved.kind {
3693            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3694                let (
3695                    UnsolvedExpr::Unknown(center_x),
3696                    UnsolvedExpr::Unknown(center_y),
3697                    UnsolvedExpr::Unknown(start_x),
3698                    UnsolvedExpr::Unknown(start_y),
3699                    UnsolvedExpr::Unknown(end_x),
3700                    UnsolvedExpr::Unknown(end_y),
3701                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3702                else {
3703                    return Err(KclError::new_semantic(KclErrorDetails::new(
3704                        "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
3705                        vec![range],
3706                    )));
3707                };
3708                Ok((
3709                    EqualRadiusInput::Radius(RadiusInputVars {
3710                        center: [*center_x, *center_y],
3711                        start: [*start_x, *start_y],
3712                        end: Some([*end_x, *end_y]),
3713                    }),
3714                    unsolved.object_id,
3715                ))
3716            }
3717            UnsolvedSegmentKind::Circle { center, start, .. } => {
3718                let (
3719                    UnsolvedExpr::Unknown(center_x),
3720                    UnsolvedExpr::Unknown(center_y),
3721                    UnsolvedExpr::Unknown(start_x),
3722                    UnsolvedExpr::Unknown(start_y),
3723                ) = (&center[0], &center[1], &start[0], &start[1])
3724                else {
3725                    return Err(KclError::new_semantic(KclErrorDetails::new(
3726                        "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
3727                        vec![range],
3728                    )));
3729                };
3730                Ok((
3731                    EqualRadiusInput::Radius(RadiusInputVars {
3732                        center: [*center_x, *center_y],
3733                        start: [*start_x, *start_y],
3734                        end: None,
3735                    }),
3736                    unsolved.object_id,
3737                ))
3738            }
3739            other => Err(KclError::new_semantic(KclErrorDetails::new(
3740                format!(
3741                    "equalRadius() currently supports only arc and circle segments, you provided {}",
3742                    other.human_friendly_kind_with_article()
3743                ),
3744                vec![range],
3745            ))),
3746        }
3747    }
3748
3749    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3750        "input",
3751        &RuntimeType::Array(
3752            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3753            ArrayLen::Minimum(2),
3754        ),
3755        exec_state,
3756    )?;
3757    let range = args.source_range;
3758
3759    let extracted_input = input
3760        .iter()
3761        .map(|segment_value| extract_equal_radius_input(segment_value, range))
3762        .collect::<Result<Vec<_>, _>>()?;
3763    let radius_inputs: Vec<RadiusInputVars> = extracted_input
3764        .iter()
3765        .map(|(equal_radius_input, _)| match equal_radius_input {
3766            EqualRadiusInput::Radius(radius_input) => *radius_input,
3767        })
3768        .collect();
3769    let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
3770
3771    let sketch_var_ty = solver_numeric_type(exec_state);
3772    let constraint_id = exec_state.next_object_id();
3773
3774    let sketch_vars = {
3775        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3776            return Err(KclError::new_semantic(KclErrorDetails::new(
3777                "equalRadius() can only be used inside a sketch block".to_owned(),
3778                vec![range],
3779            )));
3780        };
3781        sketch_state.sketch_vars.clone()
3782    };
3783
3784    let radius_initial_value = radius_guess(
3785        &sketch_vars,
3786        radius_inputs[0].center,
3787        radius_inputs[0].start,
3788        exec_state,
3789        range,
3790    )?;
3791
3792    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3793        return Err(KclError::new_semantic(KclErrorDetails::new(
3794            "equalRadius() can only be used inside a sketch block".to_owned(),
3795            vec![range],
3796        )));
3797    };
3798    let radius_id = sketch_state.next_sketch_var_id();
3799    sketch_state.sketch_vars.push(KclValue::SketchVar {
3800        value: Box::new(crate::execution::SketchVar {
3801            id: radius_id,
3802            initial_value: radius_initial_value,
3803            ty: sketch_var_ty,
3804            // Synthesized hidden radius for equalRadius(); no source `var` to map back to.
3805            node_path: None,
3806            meta: vec![],
3807        }),
3808    });
3809    let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3810
3811    for radius_input in radius_inputs {
3812        let center = datum_point(radius_input.center, range)?;
3813        let start = datum_point(radius_input.start, range)?;
3814        sketch_state
3815            .solver_constraints
3816            .push(SolverConstraint::DistanceVar(start, center, radius));
3817        if let Some(end) = radius_input.end {
3818            let end = datum_point(end, range)?;
3819            sketch_state
3820                .solver_constraints
3821                .push(SolverConstraint::DistanceVar(end, center, radius));
3822        }
3823    }
3824
3825    let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
3826        input: input_object_ids,
3827    });
3828    sketch_state.sketch_constraints.push(constraint_id);
3829    track_constraint(constraint_id, constraint, exec_state, &args);
3830
3831    Ok(KclValue::none())
3832}
3833
3834pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3835    let Some(Some(sketch_id)) = exec_state.sketch_block().map(|sb| sb.sketch_id) else {
3836        return Err(KclError::new_semantic(KclErrorDetails::new(
3837            "tangent() cannot be used outside a sketch block".to_owned(),
3838            vec![args.source_range],
3839        )));
3840    };
3841
3842    #[derive(Debug, Clone)]
3843    enum TangentInput {
3844        Line(LineVars),
3845        Circular(ArcVars),
3846    }
3847
3848    fn extract_tangent_input(
3849        segment_value: &KclValue,
3850        range: crate::SourceRange,
3851    ) -> Result<(TangentInput, ObjectId), KclError> {
3852        let KclValue::Segment { value: segment } = segment_value else {
3853            return Err(KclError::new_semantic(KclErrorDetails::new(
3854                "tangent() arguments must be segments".to_owned(),
3855                vec![range],
3856            )));
3857        };
3858        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3859            return Err(KclError::new_semantic(KclErrorDetails::new(
3860                "tangent() arguments must be unsolved segments".to_owned(),
3861                vec![range],
3862            )));
3863        };
3864        match &unsolved.kind {
3865            UnsolvedSegmentKind::Line { start, end, .. } => {
3866                let (
3867                    UnsolvedExpr::Unknown(start_x),
3868                    UnsolvedExpr::Unknown(start_y),
3869                    UnsolvedExpr::Unknown(end_x),
3870                    UnsolvedExpr::Unknown(end_y),
3871                ) = (&start[0], &start[1], &end[0], &end[1])
3872                else {
3873                    return Err(KclError::new_semantic(KclErrorDetails::new(
3874                        "line coordinates must be sketch vars for tangent()".to_owned(),
3875                        vec![range],
3876                    )));
3877                };
3878                Ok((
3879                    TangentInput::Line(LineVars {
3880                        start: [*start_x, *start_y],
3881                        end: [*end_x, *end_y],
3882                    }),
3883                    unsolved.object_id,
3884                ))
3885            }
3886            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3887                let (
3888                    UnsolvedExpr::Unknown(center_x),
3889                    UnsolvedExpr::Unknown(center_y),
3890                    UnsolvedExpr::Unknown(start_x),
3891                    UnsolvedExpr::Unknown(start_y),
3892                    UnsolvedExpr::Unknown(end_x),
3893                    UnsolvedExpr::Unknown(end_y),
3894                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3895                else {
3896                    return Err(KclError::new_semantic(KclErrorDetails::new(
3897                        "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
3898                        vec![range],
3899                    )));
3900                };
3901                Ok((
3902                    TangentInput::Circular(ArcVars {
3903                        center: [*center_x, *center_y],
3904                        start: [*start_x, *start_y],
3905                        end: Some([*end_x, *end_y]),
3906                    }),
3907                    unsolved.object_id,
3908                ))
3909            }
3910            UnsolvedSegmentKind::Circle { center, start, .. } => {
3911                let (
3912                    UnsolvedExpr::Unknown(center_x),
3913                    UnsolvedExpr::Unknown(center_y),
3914                    UnsolvedExpr::Unknown(start_x),
3915                    UnsolvedExpr::Unknown(start_y),
3916                ) = (&center[0], &center[1], &start[0], &start[1])
3917                else {
3918                    return Err(KclError::new_semantic(KclErrorDetails::new(
3919                        "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
3920                        vec![range],
3921                    )));
3922                };
3923                Ok((
3924                    TangentInput::Circular(ArcVars {
3925                        center: [*center_x, *center_y],
3926                        start: [*start_x, *start_y],
3927                        end: None,
3928                    }),
3929                    unsolved.object_id,
3930                ))
3931            }
3932            _ => Err(KclError::new_semantic(KclErrorDetails::new(
3933                "tangent() supports only line, arc, and circle segments".to_owned(),
3934                vec![range],
3935            ))),
3936        }
3937    }
3938
3939    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3940        "input",
3941        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3942        exec_state,
3943    )?;
3944    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
3945        KclError::new_semantic(KclErrorDetails::new(
3946            "tangent() requires exactly 2 input segments".to_owned(),
3947            vec![args.source_range],
3948        ))
3949    })?;
3950    let range = args.source_range;
3951    let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
3952    let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
3953
3954    enum TangentCase {
3955        LineCircular(LineVars, ArcVars),
3956        CircularCircular(ArcVars, ArcVars),
3957    }
3958    let tangent_case = match (input0, input1) {
3959        (TangentInput::Line(line), TangentInput::Circular(circular))
3960        | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
3961        (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
3962            TangentCase::CircularCircular(circular0, circular1)
3963        }
3964        (TangentInput::Line(_), TangentInput::Line(_)) => {
3965            return Err(KclError::new_semantic(KclErrorDetails::new(
3966                "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
3967                vec![range],
3968            )));
3969        }
3970    };
3971
3972    let sketch_var_ty = solver_numeric_type(exec_state);
3973    let constraint_id = exec_state.next_object_id();
3974
3975    let sketch_vars = {
3976        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3977            return Err(KclError::new_semantic(KclErrorDetails::new(
3978                "tangent() can only be used inside a sketch block".to_owned(),
3979                vec![range],
3980            )));
3981        };
3982        sketch_state.sketch_vars.clone()
3983    };
3984
3985    // Hidden radius vars. Empty metadata keeps them out of source write-back.
3986    match tangent_case {
3987        TangentCase::LineCircular(line, circular) => {
3988            let tangency_key = make_line_arc_tangency_key(line, circular);
3989            let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
3990                Some(ConstraintState::Tangency(TangencyMode::LineCircle(side))) => side,
3991                _ => {
3992                    let side = infer_line_tangent_side(&sketch_vars, line, circular.center, exec_state, range)?;
3993                    exec_state.set_constraint_state(
3994                        sketch_id,
3995                        tangency_key,
3996                        ConstraintState::Tangency(TangencyMode::LineCircle(side)),
3997                    );
3998                    side
3999                }
4000            };
4001            let line_p0 = datum_point(line.start, range)?;
4002            let line_p1 = datum_point(line.end, range)?;
4003            let line_datum = DatumLineSegment::new(line_p0, line_p1);
4004
4005            let center = datum_point(circular.center, range)?;
4006            let circular_start = datum_point(circular.start, range)?;
4007            let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
4008            let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
4009            let Some(sketch_state) = exec_state.sketch_block_mut() else {
4010                return Err(KclError::new_semantic(KclErrorDetails::new(
4011                    "tangent() can only be used inside a sketch block".to_owned(),
4012                    vec![range],
4013                )));
4014            };
4015            let radius_id = sketch_state.next_sketch_var_id();
4016            sketch_state.sketch_vars.push(KclValue::SketchVar {
4017                value: Box::new(crate::execution::SketchVar {
4018                    id: radius_id,
4019                    initial_value: radius_initial_value,
4020                    ty: sketch_var_ty,
4021                    // Synthesized hidden radius for tangent(); no source `var` to map back to.
4022                    node_path: None,
4023                    meta: vec![],
4024                }),
4025            });
4026            let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
4027            let circle = DatumCircle { center, radius };
4028
4029            // Tangency decomposition for Line/circular segment:
4030            // 1) Introduce a hidden radius variable r for the segment's underlying circle.
4031            // 2) Keep the segment's defining points on that circle with DistanceVar(point, center, r).
4032            // 3) Apply the native LineTangentToCircle solver constraint.
4033            sketch_state
4034                .solver_constraints
4035                .push(SolverConstraint::DistanceVar(circular_start, center, radius));
4036            if let Some(circular_end) = circular_end {
4037                sketch_state
4038                    .solver_constraints
4039                    .push(SolverConstraint::DistanceVar(circular_end, center, radius));
4040            }
4041            sketch_state
4042                .solver_constraints
4043                .push(SolverConstraint::LineTangentToCircle(line_datum, circle, tangency_side));
4044        }
4045        TangentCase::CircularCircular(circular0, circular1) => {
4046            let tangency_key = make_arc_arc_tangency_key(circular0, circular1);
4047            let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
4048                Some(ConstraintState::Tangency(TangencyMode::CircleCircle(side))) => side,
4049                _ => {
4050                    let side = infer_arc_tangent_side(&sketch_vars, circular0, circular1, exec_state, range)?;
4051                    exec_state.set_constraint_state(
4052                        sketch_id,
4053                        tangency_key,
4054                        ConstraintState::Tangency(TangencyMode::CircleCircle(side)),
4055                    );
4056                    side
4057                }
4058            };
4059            let center0 = datum_point(circular0.center, range)?;
4060            let start0 = datum_point(circular0.start, range)?;
4061            let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
4062            let radius0_initial_value =
4063                radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
4064            let center1 = datum_point(circular1.center, range)?;
4065            let start1 = datum_point(circular1.start, range)?;
4066            let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
4067            let radius1_initial_value =
4068                radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
4069            let Some(sketch_state) = exec_state.sketch_block_mut() else {
4070                return Err(KclError::new_semantic(KclErrorDetails::new(
4071                    "tangent() can only be used inside a sketch block".to_owned(),
4072                    vec![range],
4073                )));
4074            };
4075            let radius0_id = sketch_state.next_sketch_var_id();
4076            sketch_state.sketch_vars.push(KclValue::SketchVar {
4077                value: Box::new(crate::execution::SketchVar {
4078                    id: radius0_id,
4079                    initial_value: radius0_initial_value,
4080                    ty: sketch_var_ty,
4081                    // Synthesized hidden radius for tangent(); no source `var` to map back to.
4082                    node_path: None,
4083                    meta: vec![],
4084                }),
4085            });
4086            let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
4087            let circle0 = DatumCircle {
4088                center: center0,
4089                radius: radius0,
4090            };
4091
4092            let radius1_id = sketch_state.next_sketch_var_id();
4093            sketch_state.sketch_vars.push(KclValue::SketchVar {
4094                value: Box::new(crate::execution::SketchVar {
4095                    id: radius1_id,
4096                    initial_value: radius1_initial_value,
4097                    ty: sketch_var_ty,
4098                    // Synthesized hidden radius for tangent(); no source `var` to map back to.
4099                    node_path: None,
4100                    meta: vec![],
4101                }),
4102            });
4103            let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
4104            let circle1 = DatumCircle {
4105                center: center1,
4106                radius: radius1,
4107            };
4108
4109            // Tangency decomposition for circular segment/circular segment:
4110            // 1) Introduce one hidden radius variable per arc.
4111            // 2) Keep each segment's defining points on its corresponding circle.
4112            // 3) Apply the native CircleTangentToCircle solver constraint.
4113            sketch_state
4114                .solver_constraints
4115                .push(SolverConstraint::DistanceVar(start0, center0, radius0));
4116            if let Some(end0) = end0 {
4117                sketch_state
4118                    .solver_constraints
4119                    .push(SolverConstraint::DistanceVar(end0, center0, radius0));
4120            }
4121            sketch_state
4122                .solver_constraints
4123                .push(SolverConstraint::DistanceVar(start1, center1, radius1));
4124            if let Some(end1) = end1 {
4125                sketch_state
4126                    .solver_constraints
4127                    .push(SolverConstraint::DistanceVar(end1, center1, radius1));
4128            }
4129            sketch_state
4130                .solver_constraints
4131                .push(SolverConstraint::CircleTangentToCircle(circle0, circle1, tangency_side));
4132        }
4133    }
4134
4135    let constraint = crate::front::Constraint::Tangent(Tangent {
4136        input: vec![input0_object_id, input1_object_id],
4137    });
4138    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4139        return Err(KclError::new_semantic(KclErrorDetails::new(
4140            "tangent() can only be used inside a sketch block".to_owned(),
4141            vec![range],
4142        )));
4143    };
4144    sketch_state.sketch_constraints.push(constraint_id);
4145    track_constraint(constraint_id, constraint, exec_state, &args);
4146
4147    Ok(KclValue::none())
4148}
4149
4150#[derive(Debug, Clone, Copy)]
4151struct SymmetricPointVars {
4152    coords: [SketchVarId; 2],
4153    object_id: ObjectId,
4154}
4155
4156/// The line that geometry should be symmetric across.
4157#[derive(Debug, Clone, Copy)]
4158struct SymmetricLineVars {
4159    start: [SketchVarId; 2],
4160    end: [SketchVarId; 2],
4161    object_id: ObjectId,
4162}
4163
4164#[derive(Debug, Clone, Copy)]
4165struct SymmetricArcVars {
4166    center: [SketchVarId; 2],
4167    start: [SketchVarId; 2],
4168    end: [SketchVarId; 2],
4169    object_id: ObjectId,
4170}
4171
4172#[derive(Debug, Clone, Copy)]
4173struct SymmetricCircleVars {
4174    center: [SketchVarId; 2],
4175    start: [SketchVarId; 2],
4176    object_id: ObjectId,
4177}
4178
4179#[derive(Debug, Clone, Copy)]
4180enum SymmetricInput {
4181    Point(SymmetricPointVars),
4182    Line(SymmetricLineVars),
4183    Arc(SymmetricArcVars),
4184    Circle(SymmetricCircleVars),
4185}
4186
4187impl SymmetricInput {
4188    fn type_name(self) -> &'static str {
4189        match self {
4190            SymmetricInput::Point(_) => "points",
4191            SymmetricInput::Line(_) => "lines",
4192            SymmetricInput::Arc(_) => "arcs",
4193            SymmetricInput::Circle(_) => "circles",
4194        }
4195    }
4196
4197    fn object_id(self) -> ObjectId {
4198        match self {
4199            SymmetricInput::Point(point) => point.object_id,
4200            SymmetricInput::Line(line) => line.object_id,
4201            SymmetricInput::Arc(arc) => arc.object_id,
4202            SymmetricInput::Circle(circle) => circle.object_id,
4203        }
4204    }
4205}
4206
4207fn extract_symmetric_input(segment_value: &KclValue, range: crate::SourceRange) -> Result<SymmetricInput, KclError> {
4208    let KclValue::Segment { value: segment } = segment_value else {
4209        return Err(KclError::new_semantic(KclErrorDetails::new(
4210            format!(
4211                "symmetric() arguments must be point, line, arc, or circle segments, but found {}",
4212                segment_value.human_friendly_type()
4213            ),
4214            vec![range],
4215        )));
4216    };
4217    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4218        return Err(KclError::new_semantic(KclErrorDetails::new(
4219            "symmetric() arguments must be unsolved segments".to_owned(),
4220            vec![range],
4221        )));
4222    };
4223
4224    match &unsolved.kind {
4225        UnsolvedSegmentKind::Point { position, .. } => {
4226            let (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) = (&position[0], &position[1]) else {
4227                return Err(KclError::new_semantic(KclErrorDetails::new(
4228                    "point coordinates must be sketch vars for symmetric()".to_owned(),
4229                    vec![range],
4230                )));
4231            };
4232            Ok(SymmetricInput::Point(SymmetricPointVars {
4233                coords: [*x, *y],
4234                object_id: unsolved.object_id,
4235            }))
4236        }
4237        UnsolvedSegmentKind::Line { start, end, .. } => {
4238            let (
4239                UnsolvedExpr::Unknown(start_x),
4240                UnsolvedExpr::Unknown(start_y),
4241                UnsolvedExpr::Unknown(end_x),
4242                UnsolvedExpr::Unknown(end_y),
4243            ) = (&start[0], &start[1], &end[0], &end[1])
4244            else {
4245                return Err(KclError::new_semantic(KclErrorDetails::new(
4246                    "line coordinates must be sketch vars for symmetric()".to_owned(),
4247                    vec![range],
4248                )));
4249            };
4250            Ok(SymmetricInput::Line(SymmetricLineVars {
4251                start: [*start_x, *start_y],
4252                end: [*end_x, *end_y],
4253                object_id: unsolved.object_id,
4254            }))
4255        }
4256        UnsolvedSegmentKind::Arc { center, start, end, .. } => {
4257            let (
4258                UnsolvedExpr::Unknown(center_x),
4259                UnsolvedExpr::Unknown(center_y),
4260                UnsolvedExpr::Unknown(start_x),
4261                UnsolvedExpr::Unknown(start_y),
4262                UnsolvedExpr::Unknown(end_x),
4263                UnsolvedExpr::Unknown(end_y),
4264            ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
4265            else {
4266                return Err(KclError::new_semantic(KclErrorDetails::new(
4267                    "arc center/start/end coordinates must be sketch vars for symmetric()".to_owned(),
4268                    vec![range],
4269                )));
4270            };
4271            Ok(SymmetricInput::Arc(SymmetricArcVars {
4272                center: [*center_x, *center_y],
4273                start: [*start_x, *start_y],
4274                end: [*end_x, *end_y],
4275                object_id: unsolved.object_id,
4276            }))
4277        }
4278        UnsolvedSegmentKind::Circle { center, start, .. } => {
4279            let (
4280                UnsolvedExpr::Unknown(center_x),
4281                UnsolvedExpr::Unknown(center_y),
4282                UnsolvedExpr::Unknown(start_x),
4283                UnsolvedExpr::Unknown(start_y),
4284            ) = (&center[0], &center[1], &start[0], &start[1])
4285            else {
4286                return Err(KclError::new_semantic(KclErrorDetails::new(
4287                    "circle center/start coordinates must be sketch vars for symmetric()".to_owned(),
4288                    vec![range],
4289                )));
4290            };
4291            Ok(SymmetricInput::Circle(SymmetricCircleVars {
4292                center: [*center_x, *center_y],
4293                start: [*start_x, *start_y],
4294                object_id: unsolved.object_id,
4295            }))
4296        }
4297        UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_semantic(KclErrorDetails::new(
4298            "symmetric() does not yet support control point spline segments".to_owned(),
4299            vec![range],
4300        ))),
4301    }
4302}
4303
4304fn extract_symmetric_axis_line(
4305    segment_value: &KclValue,
4306    range: crate::SourceRange,
4307) -> Result<SymmetricLineVars, KclError> {
4308    let KclValue::Segment { value: segment } = segment_value else {
4309        return Err(KclError::new_semantic(KclErrorDetails::new(
4310            format!(
4311                "symmetric() axis must be a line Segment, but found {}",
4312                segment_value.human_friendly_type()
4313            ),
4314            vec![range],
4315        )));
4316    };
4317    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4318        return Err(KclError::new_semantic(KclErrorDetails::new(
4319            "symmetric() axis must be an unsolved line Segment".to_owned(),
4320            vec![range],
4321        )));
4322    };
4323    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
4324        return Err(KclError::new_semantic(KclErrorDetails::new(
4325            "symmetric() axis must be a line Segment".to_owned(),
4326            vec![range],
4327        )));
4328    };
4329    let (
4330        UnsolvedExpr::Unknown(start_x),
4331        UnsolvedExpr::Unknown(start_y),
4332        UnsolvedExpr::Unknown(end_x),
4333        UnsolvedExpr::Unknown(end_y),
4334    ) = (&start[0], &start[1], &end[0], &end[1])
4335    else {
4336        return Err(KclError::new_semantic(KclErrorDetails::new(
4337            "symmetric() axis line coordinates must be sketch vars".to_owned(),
4338            vec![range],
4339        )));
4340    };
4341
4342    Ok(SymmetricLineVars {
4343        start: [*start_x, *start_y],
4344        end: [*end_x, *end_y],
4345        object_id: unsolved.object_id,
4346    })
4347}
4348
4349pub async fn symmetric(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4350    #[derive(Debug, Clone, Copy)]
4351    struct SymmetricCircularVars {
4352        center: [SketchVarId; 2],
4353        start: [SketchVarId; 2],
4354        end: Option<[SketchVarId; 2]>,
4355    }
4356
4357    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
4358        "input",
4359        &RuntimeType::Array(
4360            Box::new(RuntimeType::Primitive(PrimitiveType::Segment)),
4361            ArrayLen::Known(2),
4362        ),
4363        exec_state,
4364    )?;
4365    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
4366        KclError::new_semantic(KclErrorDetails::new(
4367            "symmetric() requires exactly 2 input segments".to_owned(),
4368            vec![args.source_range],
4369        ))
4370    })?;
4371    let axis: KclValue = args.get_kw_arg("axis", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
4372    let range = args.source_range;
4373
4374    let input0 = extract_symmetric_input(&item0, range)?;
4375    let input1 = extract_symmetric_input(&item1, range)?;
4376    let axis_line = extract_symmetric_axis_line(&axis, range)?;
4377
4378    let solver_axis = DatumLineSegment::new(datum_point(axis_line.start, range)?, datum_point(axis_line.end, range)?);
4379
4380    let (mut solver_constraints, circular_inputs) = match (input0, input1) {
4381        (SymmetricInput::Point(point0), SymmetricInput::Point(point1)) => (
4382            vec![SolverConstraint::Symmetric(
4383                solver_axis,
4384                datum_point(point0.coords, range)?,
4385                datum_point(point1.coords, range)?,
4386            )],
4387            None,
4388        ),
4389        (SymmetricInput::Line(line0), SymmetricInput::Line(line1)) => {
4390            let sketch_vars = {
4391                let Some(sketch_state) = exec_state.sketch_block_mut() else {
4392                    return Err(KclError::new_semantic(KclErrorDetails::new(
4393                        "symmetric() can only be used inside a sketch block".to_owned(),
4394                        vec![range],
4395                    )));
4396                };
4397                sketch_state.sketch_vars.clone()
4398            };
4399            let mirrored_start = symmetric_hidden_point_guess(&sketch_vars, line0.start, axis_line, exec_state, range)?;
4400            let mirrored_end = symmetric_hidden_point_guess(&sketch_vars, line0.end, axis_line, exec_state, range)?;
4401            let hidden_start = create_hidden_point(exec_state, mirrored_start, range)?;
4402            let hidden_end = create_hidden_point(exec_state, mirrored_end, range)?;
4403            let mirrored_support_line =
4404                DatumLineSegment::new(datum_point(hidden_start, range)?, datum_point(hidden_end, range)?);
4405            let solver_line1 = DatumLineSegment::new(datum_point(line1.start, range)?, datum_point(line1.end, range)?);
4406
4407            (
4408                vec![
4409                    SolverConstraint::Symmetric(
4410                        solver_axis,
4411                        datum_point(line0.start, range)?,
4412                        datum_point(hidden_start, range)?,
4413                    ),
4414                    SolverConstraint::Symmetric(
4415                        solver_axis,
4416                        datum_point(line0.end, range)?,
4417                        datum_point(hidden_end, range)?,
4418                    ),
4419                    SolverConstraint::LinesAtAngle(mirrored_support_line, solver_line1, AngleKind::Parallel),
4420                    // Keep the second segment on the mirrored support line without
4421                    // forcing its endpoints to be pairwise mirrored.
4422                    SolverConstraint::PointLineDistance(datum_point(line1.start, range)?, mirrored_support_line, 0.0),
4423                ],
4424                None,
4425            )
4426        }
4427        (SymmetricInput::Arc(arc0), SymmetricInput::Arc(arc1)) => (
4428            vec![SolverConstraint::Symmetric(
4429                solver_axis,
4430                datum_point(arc0.center, range)?,
4431                datum_point(arc1.center, range)?,
4432            )],
4433            Some([
4434                SymmetricCircularVars {
4435                    center: arc0.center,
4436                    start: arc0.start,
4437                    end: Some(arc0.end),
4438                },
4439                SymmetricCircularVars {
4440                    center: arc1.center,
4441                    start: arc1.start,
4442                    end: Some(arc1.end),
4443                },
4444            ]),
4445        ),
4446        (SymmetricInput::Circle(circle0), SymmetricInput::Circle(circle1)) => (
4447            vec![SolverConstraint::Symmetric(
4448                solver_axis,
4449                datum_point(circle0.center, range)?,
4450                datum_point(circle1.center, range)?,
4451            )],
4452            Some([
4453                SymmetricCircularVars {
4454                    center: circle0.center,
4455                    start: circle0.start,
4456                    end: None,
4457                },
4458                SymmetricCircularVars {
4459                    center: circle1.center,
4460                    start: circle1.start,
4461                    end: None,
4462                },
4463            ]),
4464        ),
4465        _ => {
4466            return Err(KclError::new_semantic(KclErrorDetails::new(
4467                format!(
4468                    "symmetric() inputs must be homogeneous. You provided {} and {}",
4469                    input0.type_name(),
4470                    input1.type_name()
4471                ),
4472                vec![range],
4473            )));
4474        }
4475    };
4476
4477    if let Some([circular0, circular1]) = circular_inputs {
4478        let sketch_var_ty = solver_numeric_type(exec_state);
4479        let sketch_vars = {
4480            let Some(sketch_state) = exec_state.sketch_block_mut() else {
4481                return Err(KclError::new_semantic(KclErrorDetails::new(
4482                    "symmetric() can only be used inside a sketch block".to_owned(),
4483                    vec![range],
4484                )));
4485            };
4486            sketch_state.sketch_vars.clone()
4487        };
4488        let radius_initial_value = radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
4489
4490        let Some(sketch_state) = exec_state.sketch_block_mut() else {
4491            return Err(KclError::new_semantic(KclErrorDetails::new(
4492                "symmetric() can only be used inside a sketch block".to_owned(),
4493                vec![range],
4494            )));
4495        };
4496        let radius_id = sketch_state.next_sketch_var_id();
4497        sketch_state.sketch_vars.push(KclValue::SketchVar {
4498            value: Box::new(crate::execution::SketchVar {
4499                id: radius_id,
4500                initial_value: radius_initial_value,
4501                ty: sketch_var_ty,
4502                // Synthesized shared radius for equalRadius() across circulars; not source-backed.
4503                node_path: None,
4504                meta: vec![],
4505            }),
4506        });
4507        let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
4508
4509        for circular in [circular0, circular1] {
4510            let center = datum_point(circular.center, range)?;
4511            let start = datum_point(circular.start, range)?;
4512            solver_constraints.push(SolverConstraint::DistanceVar(start, center, radius));
4513            if let Some(end) = circular.end {
4514                let end = datum_point(end, range)?;
4515                solver_constraints.push(SolverConstraint::DistanceVar(end, center, radius));
4516            }
4517        }
4518    }
4519
4520    let constraint_id = exec_state.next_object_id();
4521    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4522        return Err(KclError::new_semantic(KclErrorDetails::new(
4523            "symmetric() can only be used inside a sketch block".to_owned(),
4524            vec![range],
4525        )));
4526    };
4527    sketch_state.solver_constraints.extend(solver_constraints);
4528
4529    let constraint = crate::front::Constraint::Symmetric(Symmetric {
4530        input: vec![input0.object_id(), input1.object_id()],
4531        axis: axis_line.object_id,
4532    });
4533    sketch_state.sketch_constraints.push(constraint_id);
4534    track_constraint(constraint_id, constraint, exec_state, &args);
4535
4536    Ok(KclValue::none())
4537}
4538
4539#[derive(Debug, Clone, Copy)]
4540pub(crate) enum LinesAtAngleKind {
4541    Parallel,
4542    Perpendicular,
4543}
4544
4545impl LinesAtAngleKind {
4546    pub fn to_function_name(self) -> &'static str {
4547        match self {
4548            LinesAtAngleKind::Parallel => "parallel",
4549            LinesAtAngleKind::Perpendicular => "perpendicular",
4550        }
4551    }
4552
4553    fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
4554        match self {
4555            LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
4556            LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
4557        }
4558    }
4559
4560    fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
4561        match self {
4562            LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
4563            LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
4564        }
4565    }
4566}
4567
4568/// Convert between two different libraries with similar angle representations
4569#[expect(unused)]
4570fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
4571    kcmc::shared::Angle::from_degrees(angle.to_degrees())
4572}
4573
4574/// Convert between two different libraries with similar angle representations
4575#[expect(unused)]
4576fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
4577    ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
4578}
4579
4580pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4581    #[derive(Clone, Copy)]
4582    struct ConstrainableLine {
4583        solver_line: DatumLineSegment,
4584        object_id: ObjectId,
4585    }
4586
4587    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
4588        "lines",
4589        &RuntimeType::Array(
4590            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
4591            ArrayLen::Minimum(2),
4592        ),
4593        exec_state,
4594    )?;
4595    let range = args.source_range;
4596    let constrainable_lines: Vec<ConstrainableLine> = lines
4597        .iter()
4598        .map(|line| {
4599            let KclValue::Segment { value: segment } = line else {
4600                return Err(KclError::new_semantic(KclErrorDetails::new(
4601                    "line argument must be a Segment".to_owned(),
4602                    vec![args.source_range],
4603                )));
4604            };
4605            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4606                return Err(KclError::new_internal(KclErrorDetails::new(
4607                    "line must be an unsolved Segment".to_owned(),
4608                    vec![args.source_range],
4609                )));
4610            };
4611            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
4612                return Err(KclError::new_semantic(KclErrorDetails::new(
4613                    "line argument must be a line, no other type of Segment".to_owned(),
4614                    vec![args.source_range],
4615                )));
4616            };
4617            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
4618                return Err(KclError::new_semantic(KclErrorDetails::new(
4619                    "line's start x coordinate must be a var".to_owned(),
4620                    vec![args.source_range],
4621                )));
4622            };
4623            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
4624                return Err(KclError::new_semantic(KclErrorDetails::new(
4625                    "line's start y coordinate must be a var".to_owned(),
4626                    vec![args.source_range],
4627                )));
4628            };
4629            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
4630                return Err(KclError::new_semantic(KclErrorDetails::new(
4631                    "line's end x coordinate must be a var".to_owned(),
4632                    vec![args.source_range],
4633                )));
4634            };
4635            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
4636                return Err(KclError::new_semantic(KclErrorDetails::new(
4637                    "line's end y coordinate must be a var".to_owned(),
4638                    vec![args.source_range],
4639                )));
4640            };
4641
4642            let solver_line_p0 =
4643                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
4644            let solver_line_p1 =
4645                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
4646
4647            Ok(ConstrainableLine {
4648                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
4649                object_id: unsolved.object_id,
4650            })
4651        })
4652        .collect::<Result<_, _>>()?;
4653
4654    let constraint_id = exec_state.next_object_id();
4655    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4656        return Err(KclError::new_semantic(KclErrorDetails::new(
4657            "parallel() can only be used inside a sketch block".to_owned(),
4658            vec![args.source_range],
4659        )));
4660    };
4661
4662    let n = constrainable_lines.len();
4663    let mut constrainable_lines_iter = constrainable_lines.iter();
4664    let first_line = constrainable_lines_iter
4665        .next()
4666        .ok_or(KclError::new_semantic(KclErrorDetails::new(
4667            format!("parallel() requires at least 2 lines, but you provided {}", n),
4668            vec![args.source_range],
4669        )))?;
4670    for line in constrainable_lines_iter {
4671        sketch_state.solver_constraints.push(SolverConstraint::LinesAtAngle(
4672            first_line.solver_line,
4673            line.solver_line,
4674            AngleKind::Parallel,
4675        ));
4676    }
4677    let constraint = Constraint::Parallel(Parallel {
4678        lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
4679    });
4680    sketch_state.sketch_constraints.push(constraint_id);
4681    track_constraint(constraint_id, constraint, exec_state, &args);
4682    Ok(KclValue::none())
4683}
4684
4685pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4686    lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
4687}
4688
4689/// A way to constrain points, or a line.
4690#[derive(Debug, Clone, Copy)]
4691enum AxisConstraintKind {
4692    Horizontal,
4693    Vertical,
4694}
4695
4696impl AxisConstraintKind {
4697    /// Which KCL function this corresponds to.
4698    fn function_name(self) -> &'static str {
4699        match self {
4700            AxisConstraintKind::Horizontal => "horizontal",
4701            AxisConstraintKind::Vertical => "vertical",
4702        }
4703    }
4704
4705    /// Use this constraint to align a line.
4706    fn line_constraint(self, line: DatumLineSegment) -> SolverConstraint {
4707        match self {
4708            AxisConstraintKind::Horizontal => SolverConstraint::Horizontal(line),
4709            AxisConstraintKind::Vertical => SolverConstraint::Vertical(line),
4710        }
4711    }
4712
4713    /// Use this constraint to align a pair of points.
4714    fn point_pair_constraint(self, p0: DatumPoint, p1: DatumPoint) -> SolverConstraint {
4715        match self {
4716            // A horizontal point set means all Y values are equal.
4717            AxisConstraintKind::Horizontal => SolverConstraint::VerticalDistance(p1, p0, 0.0),
4718            // A vertical point set means all X values are equal.
4719            AxisConstraintKind::Vertical => SolverConstraint::HorizontalDistance(p1, p0, 0.0),
4720        }
4721    }
4722
4723    /// Use this constraint to align a point to some known X or Y.
4724    fn constraint_aligning_point_to_constant(self, p0: DatumPoint, fixed_point: (f64, f64)) -> SolverConstraint {
4725        match self {
4726            AxisConstraintKind::Horizontal => SolverConstraint::Fixed(p0.y_id, fixed_point.1),
4727            AxisConstraintKind::Vertical => SolverConstraint::Fixed(p0.x_id, fixed_point.0),
4728        }
4729    }
4730
4731    fn line_artifact_constraint(self, line: ObjectId) -> Constraint {
4732        match self {
4733            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Line { line }),
4734            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Line { line }),
4735        }
4736    }
4737
4738    fn point_artifact_constraint(self, points: Vec<ConstraintSegment>) -> Constraint {
4739        match self {
4740            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Points { points }),
4741            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Points { points }),
4742        }
4743    }
4744}
4745
4746/// The line the user wants to align vertically/horizontally.
4747/// Extracted from KCL arguments.
4748#[derive(Debug, Clone, Copy)]
4749struct AxisLineVars {
4750    start: [SketchVarId; 2],
4751    end: [SketchVarId; 2],
4752    object_id: ObjectId,
4753}
4754
4755fn extract_axis_line_vars(
4756    segment: &AbstractSegment,
4757    kind: AxisConstraintKind,
4758    source_range: crate::SourceRange,
4759) -> Result<AxisLineVars, KclError> {
4760    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4761        return Err(KclError::new_internal(KclErrorDetails::new(
4762            "line must be an unsolved Segment".to_owned(),
4763            vec![source_range],
4764        )));
4765    };
4766    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
4767        return Err(KclError::new_semantic(KclErrorDetails::new(
4768            format!(
4769                "{}() line argument must be a line, no other type of Segment",
4770                kind.function_name()
4771            ),
4772            vec![source_range],
4773        )));
4774    };
4775    let (
4776        UnsolvedExpr::Unknown(start_x),
4777        UnsolvedExpr::Unknown(start_y),
4778        UnsolvedExpr::Unknown(end_x),
4779        UnsolvedExpr::Unknown(end_y),
4780    ) = (&start[0], &start[1], &end[0], &end[1])
4781    else {
4782        return Err(KclError::new_semantic(KclErrorDetails::new(
4783            "line's x and y coordinates of both start and end must be vars".to_owned(),
4784            vec![source_range],
4785        )));
4786    };
4787
4788    Ok(AxisLineVars {
4789        start: [*start_x, *start_y],
4790        end: [*end_x, *end_y],
4791        object_id: unsolved.object_id,
4792    })
4793}
4794
4795#[derive(Debug, Clone)]
4796enum PointToAlign {
4797    /// Variable point that could be constrained.
4798    Variable { x: SketchVarId, y: SketchVarId },
4799    /// Fixed millimeter constant.
4800    Fixed { x: TyF64, y: TyF64 },
4801}
4802
4803impl From<[SketchVarId; 2]> for PointToAlign {
4804    fn from(sketch_var: [SketchVarId; 2]) -> Self {
4805        Self::Variable {
4806            x: sketch_var[0],
4807            y: sketch_var[1],
4808        }
4809    }
4810}
4811
4812impl From<[TyF64; 2]> for PointToAlign {
4813    fn from([x, y]: [TyF64; 2]) -> Self {
4814        Self::Fixed { x, y }
4815    }
4816}
4817
4818fn extract_axis_point_vars(
4819    input: &KclValue,
4820    kind: AxisConstraintKind,
4821    source_range: crate::SourceRange,
4822) -> Result<PointToAlign, KclError> {
4823    match input {
4824        KclValue::Segment { value: segment } => {
4825            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4826                return Err(KclError::new_semantic(KclErrorDetails::new(
4827                    format!(
4828                        "The `{}` function point arguments must be unsolved points",
4829                        kind.function_name()
4830                    ),
4831                    vec![source_range],
4832                )));
4833            };
4834            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
4835                return Err(KclError::new_semantic(KclErrorDetails::new(
4836                    format!(
4837                        "The `{}` function list arguments must be points, but one item is {}",
4838                        kind.function_name(),
4839                        unsolved.kind.human_friendly_kind_with_article()
4840                    ),
4841                    vec![source_range],
4842                )));
4843            };
4844            match (&position[0], &position[1]) {
4845                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed {
4846                    x: x.to_owned(),
4847                    y: y.to_owned(),
4848                }),
4849                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x: *x, y: *y }),
4850                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4851                    Err(KclError::new_semantic(KclErrorDetails::new(
4852                        format!(
4853                            "The `{}` function cannot take a fixed X component and a variable Y component",
4854                            kind.function_name()
4855                        ),
4856                        vec![source_range],
4857                    )))
4858                }
4859                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4860                    Err(KclError::new_semantic(KclErrorDetails::new(
4861                        format!(
4862                            "The `{}` function cannot take a fixed X component and a variable Y component",
4863                            kind.function_name()
4864                        ),
4865                        vec![source_range],
4866                    )))
4867                }
4868            }
4869        }
4870        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4871            let [x_value, y_value] = value.as_slice() else {
4872                return Err(KclError::new_semantic(KclErrorDetails::new(
4873                    format!(
4874                        "The `{}` function point arguments must each be a Point2d like [var 0mm, var 0mm]",
4875                        kind.function_name()
4876                    ),
4877                    vec![source_range],
4878                )));
4879            };
4880            let Some(x_expr) = x_value.as_unsolved_expr() else {
4881                return Err(KclError::new_semantic(KclErrorDetails::new(
4882                    format!(
4883                        "The `{}` function point x coordinate must be a number or sketch var",
4884                        kind.function_name()
4885                    ),
4886                    vec![source_range],
4887                )));
4888            };
4889            let Some(y_expr) = y_value.as_unsolved_expr() else {
4890                return Err(KclError::new_semantic(KclErrorDetails::new(
4891                    format!(
4892                        "The `{}` function point y coordinate must be a number or sketch var",
4893                        kind.function_name()
4894                    ),
4895                    vec![source_range],
4896                )));
4897            };
4898            match (x_expr, y_expr) {
4899                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed { x, y }),
4900                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x, y }),
4901                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4902                    Err(KclError::new_semantic(KclErrorDetails::new(
4903                        format!(
4904                            "The `{}` function cannot take a fixed X component and a variable Y component",
4905                            kind.function_name()
4906                        ),
4907                        vec![source_range],
4908                    )))
4909                }
4910                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4911                    Err(KclError::new_semantic(KclErrorDetails::new(
4912                        format!(
4913                            "The `{}` function cannot take a fixed X component and a variable Y component",
4914                            kind.function_name()
4915                        ),
4916                        vec![source_range],
4917                    )))
4918                }
4919            }
4920        }
4921        _ => Err(KclError::new_semantic(KclErrorDetails::new(
4922            format!(
4923                "The `{}` function accepts either a line Segment or a list of points",
4924                kind.function_name()
4925            ),
4926            vec![source_range],
4927        ))),
4928    }
4929}
4930
4931async fn axis_constraint(
4932    kind: AxisConstraintKind,
4933    exec_state: &mut ExecState,
4934    args: Args,
4935) -> Result<KclValue, KclError> {
4936    let input: KclValue =
4937        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
4938
4939    // User could pass in a single line, or a sequence of points.
4940    match input {
4941        KclValue::Segment { value } => {
4942            // Single-line case.
4943            axis_constraint_line(value, kind, exec_state, args)
4944        }
4945        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4946            // Sequence of points case.
4947            axis_constraint_points(value, kind, exec_state, args)
4948        }
4949        other => Err(KclError::new_semantic(KclErrorDetails::new(
4950            format!(
4951                "{}() accepts either a line Segment or a list of at least two points, but you provided {}",
4952                kind.function_name(),
4953                other.human_friendly_type(),
4954            ),
4955            vec![args.source_range],
4956        ))),
4957    }
4958}
4959
4960/// User has provided a single line to align along the given axis.
4961fn axis_constraint_line(
4962    segment: Box<AbstractSegment>,
4963    kind: AxisConstraintKind,
4964    exec_state: &mut ExecState,
4965    args: Args,
4966) -> Result<KclValue, KclError> {
4967    let line = extract_axis_line_vars(&segment, kind, args.source_range)?;
4968    let range = args.source_range;
4969    let solver_p0 = DatumPoint::new_xy(
4970        line.start[0].to_constraint_id(range)?,
4971        line.start[1].to_constraint_id(range)?,
4972    );
4973    let solver_p1 = DatumPoint::new_xy(
4974        line.end[0].to_constraint_id(range)?,
4975        line.end[1].to_constraint_id(range)?,
4976    );
4977    let solver_line = DatumLineSegment::new(solver_p0, solver_p1);
4978    let constraint = kind.line_constraint(solver_line);
4979    let constraint_id = exec_state.next_object_id();
4980    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4981        return Err(KclError::new_semantic(KclErrorDetails::new(
4982            format!("{}() can only be used inside a sketch block", kind.function_name()),
4983            vec![args.source_range],
4984        )));
4985    };
4986    sketch_state.solver_constraints.push(constraint);
4987    let constraint = kind.line_artifact_constraint(line.object_id);
4988    sketch_state.sketch_constraints.push(constraint_id);
4989    track_constraint(constraint_id, constraint, exec_state, &args);
4990    Ok(KclValue::none())
4991}
4992
4993/// User has provided a sequence of points to align along the given axis.
4994fn axis_constraint_points(
4995    point_values: Vec<KclValue>,
4996    kind: AxisConstraintKind,
4997    exec_state: &mut ExecState,
4998    args: Args,
4999) -> Result<KclValue, KclError> {
5000    if point_values.len() < 2 {
5001        return Err(KclError::new_semantic(KclErrorDetails::new(
5002            format!("{}() point list must contain at least two points", kind.function_name()),
5003            vec![args.source_range],
5004        )));
5005    }
5006
5007    let trackable_point_ids = point_values
5008        .iter()
5009        .map(|point| match point {
5010            KclValue::Segment { value: segment } => {
5011                let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
5012                    return None;
5013                };
5014                let UnsolvedSegmentKind::Point { .. } = &unsolved.kind else {
5015                    return None;
5016                };
5017                Some(ConstraintSegment::from(unsolved.object_id))
5018            }
5019            point if point2d_is_origin(point) => Some(ConstraintSegment::ORIGIN),
5020            _ => None,
5021        })
5022        .collect::<Option<Vec<_>>>();
5023
5024    let Some(sketch_state) = exec_state.sketch_block_mut() else {
5025        return Err(KclError::new_semantic(KclErrorDetails::new(
5026            format!("{}() can only be used inside a sketch block", kind.function_name()),
5027            vec![args.source_range],
5028        )));
5029    };
5030
5031    let points: Vec<PointToAlign> = point_values
5032        .iter()
5033        .map(|point| extract_axis_point_vars(point, kind, args.source_range))
5034        .collect::<Result<_, _>>()?;
5035
5036    let mut solver_constraints = Vec::with_capacity(points.len().saturating_sub(1));
5037
5038    let mut var_points = Vec::new();
5039    let mut fix_points = Vec::new();
5040    for point in points {
5041        match point {
5042            PointToAlign::Variable { x, y } => var_points.push((x, y)),
5043            PointToAlign::Fixed { x, y } => fix_points.push((x, y)),
5044        }
5045    }
5046    if fix_points.len() > 1 {
5047        return Err(KclError::new_semantic(KclErrorDetails::new(
5048            format!(
5049                "{}() point list can contain at most 1 fixed point, but you provided {}",
5050                kind.function_name(),
5051                fix_points.len()
5052            ),
5053            vec![args.source_range],
5054        )));
5055    }
5056
5057    if let Some(fix_point) = fix_points.pop() {
5058        // We have to align all the variable points with this singular fixed point.
5059        // For points 0, 1, 2, ..., n, create constraints
5060        // fixed(0.x, fix.x)
5061        // fixed(1.x, fix.x)
5062        // ...
5063        // fixed(n.x, fix.x)
5064        // (or y, whatever is appropriate)
5065        for point in var_points {
5066            let solver_point = datum_point([point.0, point.1], args.source_range)?;
5067            let fix_point_mm = (fix_point.0.to_mm(), fix_point.1.to_mm());
5068            solver_constraints.push(kind.constraint_aligning_point_to_constant(solver_point, fix_point_mm));
5069        }
5070    } else {
5071        // For points 0, 1, 2, ..., n, create constraints
5072        // vertical(0, 1)
5073        // vertical(0, 2)
5074        // ...
5075        // vertical(0, n)
5076        // (or horizontal, if appropriate)
5077        let mut points = var_points.into_iter();
5078        let first_point = points.next().ok_or_else(|| {
5079            KclError::new_semantic(KclErrorDetails::new(
5080                format!("{}() point list must contain at least two points", kind.function_name()),
5081                vec![args.source_range],
5082            ))
5083        })?;
5084        let anchor = datum_point([first_point.0, first_point.1], args.source_range)?;
5085        for point in points {
5086            let solver_point = datum_point([point.0, point.1], args.source_range)?;
5087            solver_constraints.push(kind.point_pair_constraint(anchor, solver_point));
5088        }
5089    }
5090    sketch_state.solver_constraints.extend(solver_constraints);
5091
5092    if let Some(point_ids) = trackable_point_ids {
5093        let constraint_id = exec_state.next_object_id();
5094        let Some(sketch_state) = exec_state.sketch_block_mut() else {
5095            debug_assert!(false, "Constraint created outside a sketch block");
5096            return Ok(KclValue::none());
5097        };
5098        sketch_state.sketch_constraints.push(constraint_id);
5099        let constraint = kind.point_artifact_constraint(point_ids);
5100        track_constraint(constraint_id, constraint, exec_state, &args);
5101    }
5102
5103    Ok(KclValue::none())
5104}
5105
5106pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
5107    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
5108        "lines",
5109        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
5110        exec_state,
5111    )?;
5112    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
5113        KclError::new_semantic(KclErrorDetails::new(
5114            "must have two input lines".to_owned(),
5115            vec![args.source_range],
5116        ))
5117    })?;
5118    let KclValue::Segment { value: segment0 } = &line0 else {
5119        return Err(KclError::new_semantic(KclErrorDetails::new(
5120            "line argument must be a Segment".to_owned(),
5121            vec![args.source_range],
5122        )));
5123    };
5124    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
5125        return Err(KclError::new_internal(KclErrorDetails::new(
5126            "line must be an unsolved Segment".to_owned(),
5127            vec![args.source_range],
5128        )));
5129    };
5130    let UnsolvedSegmentKind::Line {
5131        start: start0,
5132        end: end0,
5133        ..
5134    } = &unsolved0.kind
5135    else {
5136        return Err(KclError::new_semantic(KclErrorDetails::new(
5137            "line argument must be a line, no other type of Segment".to_owned(),
5138            vec![args.source_range],
5139        )));
5140    };
5141    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
5142        return Err(KclError::new_semantic(KclErrorDetails::new(
5143            "line's start x coordinate must be a var".to_owned(),
5144            vec![args.source_range],
5145        )));
5146    };
5147    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
5148        return Err(KclError::new_semantic(KclErrorDetails::new(
5149            "line's start y coordinate must be a var".to_owned(),
5150            vec![args.source_range],
5151        )));
5152    };
5153    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
5154        return Err(KclError::new_semantic(KclErrorDetails::new(
5155            "line's end x coordinate must be a var".to_owned(),
5156            vec![args.source_range],
5157        )));
5158    };
5159    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
5160        return Err(KclError::new_semantic(KclErrorDetails::new(
5161            "line's end y coordinate must be a var".to_owned(),
5162            vec![args.source_range],
5163        )));
5164    };
5165    let KclValue::Segment { value: segment1 } = &line1 else {
5166        return Err(KclError::new_semantic(KclErrorDetails::new(
5167            "line argument must be a Segment".to_owned(),
5168            vec![args.source_range],
5169        )));
5170    };
5171    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
5172        return Err(KclError::new_internal(KclErrorDetails::new(
5173            "line must be an unsolved Segment".to_owned(),
5174            vec![args.source_range],
5175        )));
5176    };
5177    let UnsolvedSegmentKind::Line {
5178        start: start1,
5179        end: end1,
5180        ..
5181    } = &unsolved1.kind
5182    else {
5183        return Err(KclError::new_semantic(KclErrorDetails::new(
5184            "line argument must be a line, no other type of Segment".to_owned(),
5185            vec![args.source_range],
5186        )));
5187    };
5188    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
5189        return Err(KclError::new_semantic(KclErrorDetails::new(
5190            "line's start x coordinate must be a var".to_owned(),
5191            vec![args.source_range],
5192        )));
5193    };
5194    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
5195        return Err(KclError::new_semantic(KclErrorDetails::new(
5196            "line's start y coordinate must be a var".to_owned(),
5197            vec![args.source_range],
5198        )));
5199    };
5200    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
5201        return Err(KclError::new_semantic(KclErrorDetails::new(
5202            "line's end x coordinate must be a var".to_owned(),
5203            vec![args.source_range],
5204        )));
5205    };
5206    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
5207        return Err(KclError::new_semantic(KclErrorDetails::new(
5208            "line's end y coordinate must be a var".to_owned(),
5209            vec![args.source_range],
5210        )));
5211    };
5212
5213    // All coordinates are sketch vars. Proceed.
5214    let sketch_constraint = SketchConstraint {
5215        kind: SketchConstraintKind::Angle {
5216            line0: crate::execution::ConstrainableLine2d {
5217                object_id: unsolved0.object_id,
5218                vars: [
5219                    crate::front::Point2d {
5220                        x: *line0_p0_x,
5221                        y: *line0_p0_y,
5222                    },
5223                    crate::front::Point2d {
5224                        x: *line0_p1_x,
5225                        y: *line0_p1_y,
5226                    },
5227                ],
5228            },
5229            line1: crate::execution::ConstrainableLine2d {
5230                object_id: unsolved1.object_id,
5231                vars: [
5232                    crate::front::Point2d {
5233                        x: *line1_p0_x,
5234                        y: *line1_p0_y,
5235                    },
5236                    crate::front::Point2d {
5237                        x: *line1_p1_x,
5238                        y: *line1_p1_y,
5239                    },
5240                ],
5241            },
5242        },
5243        meta: vec![args.source_range.into()],
5244    };
5245    Ok(KclValue::SketchConstraint {
5246        value: Box::new(sketch_constraint),
5247    })
5248}
5249
5250async fn lines_at_angle(
5251    angle_kind: LinesAtAngleKind,
5252    exec_state: &mut ExecState,
5253    args: Args,
5254) -> Result<KclValue, KclError> {
5255    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
5256        "lines",
5257        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
5258        exec_state,
5259    )?;
5260    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
5261        KclError::new_semantic(KclErrorDetails::new(
5262            "must have two input lines".to_owned(),
5263            vec![args.source_range],
5264        ))
5265    })?;
5266
5267    let KclValue::Segment { value: segment0 } = &line0 else {
5268        return Err(KclError::new_semantic(KclErrorDetails::new(
5269            "line argument must be a Segment".to_owned(),
5270            vec![args.source_range],
5271        )));
5272    };
5273    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
5274        return Err(KclError::new_internal(KclErrorDetails::new(
5275            "line must be an unsolved Segment".to_owned(),
5276            vec![args.source_range],
5277        )));
5278    };
5279    let UnsolvedSegmentKind::Line {
5280        start: start0,
5281        end: end0,
5282        ..
5283    } = &unsolved0.kind
5284    else {
5285        return Err(KclError::new_semantic(KclErrorDetails::new(
5286            "line argument must be a line, no other type of Segment".to_owned(),
5287            vec![args.source_range],
5288        )));
5289    };
5290    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
5291        return Err(KclError::new_semantic(KclErrorDetails::new(
5292            "line's start x coordinate must be a var".to_owned(),
5293            vec![args.source_range],
5294        )));
5295    };
5296    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
5297        return Err(KclError::new_semantic(KclErrorDetails::new(
5298            "line's start y coordinate must be a var".to_owned(),
5299            vec![args.source_range],
5300        )));
5301    };
5302    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
5303        return Err(KclError::new_semantic(KclErrorDetails::new(
5304            "line's end x coordinate must be a var".to_owned(),
5305            vec![args.source_range],
5306        )));
5307    };
5308    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
5309        return Err(KclError::new_semantic(KclErrorDetails::new(
5310            "line's end y coordinate must be a var".to_owned(),
5311            vec![args.source_range],
5312        )));
5313    };
5314    let KclValue::Segment { value: segment1 } = &line1 else {
5315        return Err(KclError::new_semantic(KclErrorDetails::new(
5316            "line argument must be a Segment".to_owned(),
5317            vec![args.source_range],
5318        )));
5319    };
5320    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
5321        return Err(KclError::new_internal(KclErrorDetails::new(
5322            "line must be an unsolved Segment".to_owned(),
5323            vec![args.source_range],
5324        )));
5325    };
5326    let UnsolvedSegmentKind::Line {
5327        start: start1,
5328        end: end1,
5329        ..
5330    } = &unsolved1.kind
5331    else {
5332        return Err(KclError::new_semantic(KclErrorDetails::new(
5333            "line argument must be a line, no other type of Segment".to_owned(),
5334            vec![args.source_range],
5335        )));
5336    };
5337    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
5338        return Err(KclError::new_semantic(KclErrorDetails::new(
5339            "line's start x coordinate must be a var".to_owned(),
5340            vec![args.source_range],
5341        )));
5342    };
5343    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
5344        return Err(KclError::new_semantic(KclErrorDetails::new(
5345            "line's start y coordinate must be a var".to_owned(),
5346            vec![args.source_range],
5347        )));
5348    };
5349    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
5350        return Err(KclError::new_semantic(KclErrorDetails::new(
5351            "line's end x coordinate must be a var".to_owned(),
5352            vec![args.source_range],
5353        )));
5354    };
5355    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
5356        return Err(KclError::new_semantic(KclErrorDetails::new(
5357            "line's end y coordinate must be a var".to_owned(),
5358            vec![args.source_range],
5359        )));
5360    };
5361
5362    let range = args.source_range;
5363    let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5364        line0_p0_x.to_constraint_id(range)?,
5365        line0_p0_y.to_constraint_id(range)?,
5366    );
5367    let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5368        line0_p1_x.to_constraint_id(range)?,
5369        line0_p1_y.to_constraint_id(range)?,
5370    );
5371    let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
5372    let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5373        line1_p0_x.to_constraint_id(range)?,
5374        line1_p0_y.to_constraint_id(range)?,
5375    );
5376    let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5377        line1_p1_x.to_constraint_id(range)?,
5378        line1_p1_y.to_constraint_id(range)?,
5379    );
5380    let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
5381    let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
5382    let constraint_id = exec_state.next_object_id();
5383    // Save the constraint to be used for solving.
5384    let Some(sketch_state) = exec_state.sketch_block_mut() else {
5385        return Err(KclError::new_semantic(KclErrorDetails::new(
5386            format!(
5387                "{}() can only be used inside a sketch block",
5388                angle_kind.to_function_name()
5389            ),
5390            vec![args.source_range],
5391        )));
5392    };
5393    sketch_state.solver_constraints.push(constraint);
5394    let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
5395    sketch_state.sketch_constraints.push(constraint_id);
5396    track_constraint(constraint_id, constraint, exec_state, &args);
5397    Ok(KclValue::none())
5398}
5399
5400pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
5401    axis_constraint(AxisConstraintKind::Horizontal, exec_state, args).await
5402}
5403
5404pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
5405    axis_constraint(AxisConstraintKind::Vertical, exec_state, args).await
5406}