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