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