Skip to main content

kcl_lib/std/
constraints.rs

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