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