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