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: 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
2286    create_circular_radius_constraint(
2287        segment,
2288        |points| SketchConstraintKind::Radius { points },
2289        args.source_range,
2290    )
2291    .map(|constraint| KclValue::SketchConstraint {
2292        value: Box::new(constraint),
2293    })
2294}
2295
2296pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2297    let segment: KclValue =
2298        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
2299
2300    create_circular_radius_constraint(
2301        segment,
2302        |points| SketchConstraintKind::Diameter { points },
2303        args.source_range,
2304    )
2305    .map(|constraint| KclValue::SketchConstraint {
2306        value: Box::new(constraint),
2307    })
2308}
2309
2310pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2311    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2312        "points",
2313        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2314        exec_state,
2315    )?;
2316    let label_position = get_constraint_label_position(exec_state, &args, "horizontalDistance")?;
2317    let [p1, p2] = points.as_slice() else {
2318        return Err(KclError::new_semantic(KclErrorDetails::new(
2319            "must have two input points".to_owned(),
2320            vec![args.source_range],
2321        )));
2322    };
2323    match (p1, p2) {
2324        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2325            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2326                return Err(KclError::new_semantic(KclErrorDetails::new(
2327                    "first point must be an unsolved segment".to_owned(),
2328                    vec![args.source_range],
2329                )));
2330            };
2331            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2332                return Err(KclError::new_semantic(KclErrorDetails::new(
2333                    "second point must be an unsolved segment".to_owned(),
2334                    vec![args.source_range],
2335                )));
2336            };
2337            match (&unsolved0.kind, &unsolved1.kind) {
2338                (
2339                    UnsolvedSegmentKind::Point { position: pos0, .. },
2340                    UnsolvedSegmentKind::Point { position: pos1, .. },
2341                ) => {
2342                    // Both segments are points. Create a horizontal distance constraint
2343                    // between them.
2344                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2345                        (
2346                            UnsolvedExpr::Unknown(p0_x),
2347                            UnsolvedExpr::Unknown(p0_y),
2348                            UnsolvedExpr::Unknown(p1_x),
2349                            UnsolvedExpr::Unknown(p1_y),
2350                        ) => {
2351                            // All coordinates are sketch vars. Proceed.
2352                            let sketch_constraint = SketchConstraint {
2353                                kind: SketchConstraintKind::HorizontalDistance {
2354                                    points: [
2355                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2356                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2357                                            object_id: unsolved0.object_id,
2358                                        }),
2359                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2360                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2361                                            object_id: unsolved1.object_id,
2362                                        }),
2363                                    ],
2364                                    label_position,
2365                                },
2366                                meta: vec![args.source_range.into()],
2367                            };
2368                            Ok(KclValue::SketchConstraint {
2369                                value: Box::new(sketch_constraint),
2370                            })
2371                        }
2372                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2373                            "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
2374                                .to_owned(),
2375                            vec![args.source_range],
2376                        ))),
2377                    }
2378                }
2379                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2380                    "horizontalDistance() arguments must be unsolved points".to_owned(),
2381                    vec![args.source_range],
2382                ))),
2383            }
2384        }
2385        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2386        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2387            if !point2d_is_origin(point2d) {
2388                return Err(KclError::new_semantic(KclErrorDetails::new(
2389                    "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
2390                    vec![args.source_range],
2391                )));
2392            }
2393
2394            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2395                return Err(KclError::new_semantic(KclErrorDetails::new(
2396                    "segment must be an unsolved segment".to_owned(),
2397                    vec![args.source_range],
2398                )));
2399            };
2400            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2401                return Err(KclError::new_semantic(KclErrorDetails::new(
2402                    "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2403                    vec![args.source_range],
2404                )));
2405            };
2406            match (&position[0], &position[1]) {
2407                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2408                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2409                        vars: crate::front::Point2d {
2410                            x: *point_x,
2411                            y: *point_y,
2412                        },
2413                        object_id: unsolved.object_id,
2414                    });
2415                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2416                        [point, ConstrainablePoint2dOrOrigin::Origin]
2417                    } else {
2418                        [ConstrainablePoint2dOrOrigin::Origin, point]
2419                    };
2420                    Ok(KclValue::SketchConstraint {
2421                        value: Box::new(SketchConstraint {
2422                            kind: SketchConstraintKind::HorizontalDistance { points, label_position },
2423                            meta: vec![args.source_range.into()],
2424                        }),
2425                    })
2426                }
2427                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2428                    "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
2429                        .to_owned(),
2430                    vec![args.source_range],
2431                ))),
2432            }
2433        }
2434        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2435            "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
2436            vec![args.source_range],
2437        ))),
2438    }
2439}
2440
2441pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2442    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2443        "points",
2444        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2445        exec_state,
2446    )?;
2447    let label_position = get_constraint_label_position(exec_state, &args, "verticalDistance")?;
2448    let [p1, p2] = points.as_slice() else {
2449        return Err(KclError::new_semantic(KclErrorDetails::new(
2450            "must have two input points".to_owned(),
2451            vec![args.source_range],
2452        )));
2453    };
2454    match (p1, p2) {
2455        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2456            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2457                return Err(KclError::new_semantic(KclErrorDetails::new(
2458                    "first point must be an unsolved segment".to_owned(),
2459                    vec![args.source_range],
2460                )));
2461            };
2462            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2463                return Err(KclError::new_semantic(KclErrorDetails::new(
2464                    "second point must be an unsolved segment".to_owned(),
2465                    vec![args.source_range],
2466                )));
2467            };
2468            match (&unsolved0.kind, &unsolved1.kind) {
2469                (
2470                    UnsolvedSegmentKind::Point { position: pos0, .. },
2471                    UnsolvedSegmentKind::Point { position: pos1, .. },
2472                ) => {
2473                    // Both segments are points. Create a vertical distance constraint
2474                    // between them.
2475                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2476                        (
2477                            UnsolvedExpr::Unknown(p0_x),
2478                            UnsolvedExpr::Unknown(p0_y),
2479                            UnsolvedExpr::Unknown(p1_x),
2480                            UnsolvedExpr::Unknown(p1_y),
2481                        ) => {
2482                            // All coordinates are sketch vars. Proceed.
2483                            let sketch_constraint = SketchConstraint {
2484                                kind: SketchConstraintKind::VerticalDistance {
2485                                    points: [
2486                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2487                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2488                                            object_id: unsolved0.object_id,
2489                                        }),
2490                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2491                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2492                                            object_id: unsolved1.object_id,
2493                                        }),
2494                                    ],
2495                                    label_position,
2496                                },
2497                                meta: vec![args.source_range.into()],
2498                            };
2499                            Ok(KclValue::SketchConstraint {
2500                                value: Box::new(sketch_constraint),
2501                            })
2502                        }
2503                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2504                            "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
2505                                .to_owned(),
2506                            vec![args.source_range],
2507                        ))),
2508                    }
2509                }
2510                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2511                    "verticalDistance() arguments must be unsolved points".to_owned(),
2512                    vec![args.source_range],
2513                ))),
2514            }
2515        }
2516        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2517            if !point2d_is_origin(point2d) {
2518                return Err(KclError::new_semantic(KclErrorDetails::new(
2519                    "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
2520                    vec![args.source_range],
2521                )));
2522            }
2523
2524            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2525                return Err(KclError::new_semantic(KclErrorDetails::new(
2526                    "segment must be an unsolved segment".to_owned(),
2527                    vec![args.source_range],
2528                )));
2529            };
2530            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2531                return Err(KclError::new_semantic(KclErrorDetails::new(
2532                    "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2533                    vec![args.source_range],
2534                )));
2535            };
2536            match (&position[0], &position[1]) {
2537                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2538                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2539                        vars: crate::front::Point2d {
2540                            x: *point_x,
2541                            y: *point_y,
2542                        },
2543                        object_id: unsolved.object_id,
2544                    });
2545                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2546                        [point, ConstrainablePoint2dOrOrigin::Origin]
2547                    } else {
2548                        [ConstrainablePoint2dOrOrigin::Origin, point]
2549                    };
2550                    Ok(KclValue::SketchConstraint {
2551                        value: Box::new(SketchConstraint {
2552                            kind: SketchConstraintKind::VerticalDistance { points, label_position },
2553                            meta: vec![args.source_range.into()],
2554                        }),
2555                    })
2556                }
2557                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2558                    "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
2559                        .to_owned(),
2560                    vec![args.source_range],
2561                ))),
2562            }
2563        }
2564        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2565            "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
2566            vec![args.source_range],
2567        ))),
2568    }
2569}
2570
2571#[derive(Debug, Clone, Copy)]
2572struct MidpointPointVars {
2573    coords: [SketchVarId; 2],
2574    #[cfg(feature = "artifact-graph")]
2575    object_id: ObjectId,
2576}
2577
2578#[derive(Debug, Clone, Copy)]
2579enum MidpointTargetVars {
2580    Line {
2581        start: [SketchVarId; 2],
2582        end: [SketchVarId; 2],
2583        #[cfg(feature = "artifact-graph")]
2584        object_id: ObjectId,
2585    },
2586    Arc {
2587        center: [SketchVarId; 2],
2588        start: [SketchVarId; 2],
2589        end: [SketchVarId; 2],
2590        #[cfg(feature = "artifact-graph")]
2591        object_id: ObjectId,
2592    },
2593}
2594
2595impl MidpointTargetVars {
2596    #[cfg(feature = "artifact-graph")]
2597    fn object_id(self) -> ObjectId {
2598        match self {
2599            Self::Line { object_id, .. } | Self::Arc { object_id, .. } => object_id,
2600        }
2601    }
2602}
2603
2604fn extract_midpoint_point(segment_value: &KclValue, range: crate::SourceRange) -> Result<MidpointPointVars, KclError> {
2605    let KclValue::Segment { value: segment } = segment_value else {
2606        return Err(KclError::new_semantic(KclErrorDetails::new(
2607            format!(
2608                "midpoint() point must be a point Segment, but found {}",
2609                segment_value.human_friendly_type()
2610            ),
2611            vec![range],
2612        )));
2613    };
2614    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2615        return Err(KclError::new_semantic(KclErrorDetails::new(
2616            "midpoint() point must be an unsolved point Segment".to_owned(),
2617            vec![range],
2618        )));
2619    };
2620    let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2621        return Err(KclError::new_semantic(KclErrorDetails::new(
2622            "midpoint() point must be a point Segment".to_owned(),
2623            vec![range],
2624        )));
2625    };
2626    let (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) = (&position[0], &position[1]) else {
2627        return Err(KclError::new_semantic(KclErrorDetails::new(
2628            "midpoint() point coordinates must be sketch vars".to_owned(),
2629            vec![range],
2630        )));
2631    };
2632
2633    Ok(MidpointPointVars {
2634        coords: [*point_x, *point_y],
2635        #[cfg(feature = "artifact-graph")]
2636        object_id: unsolved.object_id,
2637    })
2638}
2639
2640fn extract_midpoint_target(
2641    segment_value: &KclValue,
2642    range: crate::SourceRange,
2643) -> Result<MidpointTargetVars, KclError> {
2644    let KclValue::Segment { value: segment } = segment_value else {
2645        return Err(KclError::new_semantic(KclErrorDetails::new(
2646            format!(
2647                "midpoint() target must be a line or arc Segment, but found {}",
2648                segment_value.human_friendly_type()
2649            ),
2650            vec![range],
2651        )));
2652    };
2653    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2654        return Err(KclError::new_semantic(KclErrorDetails::new(
2655            "midpoint() target must be an unsolved line or arc Segment".to_owned(),
2656            vec![range],
2657        )));
2658    };
2659    match &unsolved.kind {
2660        UnsolvedSegmentKind::Line { start, end, .. } => {
2661            let (
2662                UnsolvedExpr::Unknown(start_x),
2663                UnsolvedExpr::Unknown(start_y),
2664                UnsolvedExpr::Unknown(end_x),
2665                UnsolvedExpr::Unknown(end_y),
2666            ) = (&start[0], &start[1], &end[0], &end[1])
2667            else {
2668                return Err(KclError::new_semantic(KclErrorDetails::new(
2669                    "midpoint() line coordinates must be sketch vars".to_owned(),
2670                    vec![range],
2671                )));
2672            };
2673
2674            Ok(MidpointTargetVars::Line {
2675                start: [*start_x, *start_y],
2676                end: [*end_x, *end_y],
2677                #[cfg(feature = "artifact-graph")]
2678                object_id: unsolved.object_id,
2679            })
2680        }
2681        UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2682            let (
2683                UnsolvedExpr::Unknown(center_x),
2684                UnsolvedExpr::Unknown(center_y),
2685                UnsolvedExpr::Unknown(start_x),
2686                UnsolvedExpr::Unknown(start_y),
2687                UnsolvedExpr::Unknown(end_x),
2688                UnsolvedExpr::Unknown(end_y),
2689            ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2690            else {
2691                return Err(KclError::new_semantic(KclErrorDetails::new(
2692                    "midpoint() arc center/start/end coordinates must be sketch vars".to_owned(),
2693                    vec![range],
2694                )));
2695            };
2696
2697            Ok(MidpointTargetVars::Arc {
2698                center: [*center_x, *center_y],
2699                start: [*start_x, *start_y],
2700                end: [*end_x, *end_y],
2701                #[cfg(feature = "artifact-graph")]
2702                object_id: unsolved.object_id,
2703            })
2704        }
2705        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2706            "midpoint() target must be a line or circular arc Segment".to_owned(),
2707            vec![range],
2708        ))),
2709    }
2710}
2711
2712pub async fn midpoint(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2713    let target: KclValue =
2714        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
2715    let point: KclValue = args.get_kw_arg("point", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
2716    let range = args.source_range;
2717
2718    let point = extract_midpoint_point(&point, range)?;
2719    let target = extract_midpoint_target(&target, range)?;
2720
2721    #[cfg(feature = "artifact-graph")]
2722    let constraint_id = exec_state.next_object_id();
2723    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2724        return Err(KclError::new_semantic(KclErrorDetails::new(
2725            "midpoint() can only be used inside a sketch block".to_owned(),
2726            vec![range],
2727        )));
2728    };
2729
2730    let solver_point = datum_point(point.coords, range)?;
2731    match target {
2732        MidpointTargetVars::Line { start, end, .. } => {
2733            sketch_state.solver_constraints.push(SolverConstraint::Midpoint(
2734                DatumLineSegment::new(datum_point(start, range)?, datum_point(end, range)?),
2735                solver_point,
2736            ));
2737        }
2738        MidpointTargetVars::Arc { center, start, end, .. } => {
2739            sketch_state
2740                .solver_constraints
2741                .extend(SolverConstraint::point_bisects_arc(
2742                    DatumCircularArc {
2743                        center: datum_point(center, range)?,
2744                        start: datum_point(start, range)?,
2745                        end: datum_point(end, range)?,
2746                    },
2747                    solver_point,
2748                ));
2749        }
2750    }
2751
2752    #[cfg(feature = "artifact-graph")]
2753    {
2754        let constraint = Constraint::Midpoint(Midpoint {
2755            point: point.object_id,
2756            segment: target.object_id(),
2757        });
2758        sketch_state.sketch_constraints.push(constraint_id);
2759        track_constraint(constraint_id, constraint, exec_state, &args);
2760    }
2761
2762    Ok(KclValue::none())
2763}
2764
2765pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2766    #[derive(Clone, Copy)]
2767    struct ConstrainableLine {
2768        solver_line: DatumLineSegment,
2769        #[cfg(feature = "artifact-graph")]
2770        object_id: ObjectId,
2771    }
2772
2773    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2774        "lines",
2775        &RuntimeType::Array(
2776            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2777            ArrayLen::Minimum(2),
2778        ),
2779        exec_state,
2780    )?;
2781    let range = args.source_range;
2782    let constrainable_lines: Vec<ConstrainableLine> = lines
2783        .iter()
2784        .map(|line| {
2785            let KclValue::Segment { value: segment } = line else {
2786                return Err(KclError::new_semantic(KclErrorDetails::new(
2787                    "line argument must be a Segment".to_owned(),
2788                    vec![args.source_range],
2789                )));
2790            };
2791            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2792                return Err(KclError::new_internal(KclErrorDetails::new(
2793                    "line must be an unsolved Segment".to_owned(),
2794                    vec![args.source_range],
2795                )));
2796            };
2797            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2798                return Err(KclError::new_semantic(KclErrorDetails::new(
2799                    "line argument must be a line, no other type of Segment".to_owned(),
2800                    vec![args.source_range],
2801                )));
2802            };
2803            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2804                return Err(KclError::new_semantic(KclErrorDetails::new(
2805                    "line's start x coordinate must be a var".to_owned(),
2806                    vec![args.source_range],
2807                )));
2808            };
2809            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2810                return Err(KclError::new_semantic(KclErrorDetails::new(
2811                    "line's start y coordinate must be a var".to_owned(),
2812                    vec![args.source_range],
2813                )));
2814            };
2815            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2816                return Err(KclError::new_semantic(KclErrorDetails::new(
2817                    "line's end x coordinate must be a var".to_owned(),
2818                    vec![args.source_range],
2819                )));
2820            };
2821            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2822                return Err(KclError::new_semantic(KclErrorDetails::new(
2823                    "line's end y coordinate must be a var".to_owned(),
2824                    vec![args.source_range],
2825                )));
2826            };
2827
2828            let solver_line_p0 =
2829                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2830            let solver_line_p1 =
2831                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2832
2833            Ok(ConstrainableLine {
2834                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2835                #[cfg(feature = "artifact-graph")]
2836                object_id: unsolved.object_id,
2837            })
2838        })
2839        .collect::<Result<_, _>>()?;
2840
2841    #[cfg(feature = "artifact-graph")]
2842    let constraint_id = exec_state.next_object_id();
2843    // Save the constraint to be used for solving.
2844    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2845        return Err(KclError::new_semantic(KclErrorDetails::new(
2846            "equalLength() can only be used inside a sketch block".to_owned(),
2847            vec![args.source_range],
2848        )));
2849    };
2850    let first_line = constrainable_lines[0];
2851    for line in constrainable_lines.iter().skip(1) {
2852        sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
2853            first_line.solver_line,
2854            line.solver_line,
2855        ));
2856    }
2857    #[cfg(feature = "artifact-graph")]
2858    {
2859        let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
2860            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
2861        });
2862        sketch_state.sketch_constraints.push(constraint_id);
2863        track_constraint(constraint_id, constraint, exec_state, &args);
2864    }
2865    Ok(KclValue::none())
2866}
2867
2868fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
2869    Ok(DatumPoint::new_xy(
2870        coords[0].to_constraint_id(range)?,
2871        coords[1].to_constraint_id(range)?,
2872    ))
2873}
2874
2875fn sketch_var_initial_value(
2876    sketch_vars: &[KclValue],
2877    id: SketchVarId,
2878    exec_state: &mut ExecState,
2879    range: crate::SourceRange,
2880) -> Result<f64, KclError> {
2881    sketch_vars
2882        .get(id.0)
2883        .and_then(KclValue::as_sketch_var)
2884        .map(|sketch_var| {
2885            sketch_var
2886                .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
2887                .map(|value| value.n)
2888        })
2889        .transpose()?
2890        .ok_or_else(|| {
2891            KclError::new_internal(KclErrorDetails::new(
2892                format!("Missing sketch variable initial value for id {}", id.0),
2893                vec![range],
2894            ))
2895        })
2896}
2897
2898fn radius_guess(
2899    sketch_vars: &[KclValue],
2900    center: [SketchVarId; 2],
2901    point: [SketchVarId; 2],
2902    exec_state: &mut ExecState,
2903    range: crate::SourceRange,
2904) -> Result<f64, KclError> {
2905    let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
2906        - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
2907    let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
2908        - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
2909    Ok(libm::hypot(dx, dy))
2910}
2911
2912fn reflect_point_across_line(point: [f64; 2], axis_start: [f64; 2], axis_end: [f64; 2]) -> [f64; 2] {
2913    let [px, py] = point;
2914    let [ax, ay] = axis_start;
2915    let [bx, by] = axis_end;
2916    let dx = bx - ax;
2917    let dy = by - ay;
2918    let axis_len_sq = dx * dx + dy * dy;
2919    if axis_len_sq <= f64::EPSILON {
2920        return point;
2921    }
2922
2923    let point_from_axis = [px - ax, py - ay];
2924    let projection_scale = (point_from_axis[0] * dx + point_from_axis[1] * dy) / axis_len_sq;
2925    let projected = [ax + projection_scale * dx, ay + projection_scale * dy];
2926
2927    [2.0 * projected[0] - px, 2.0 * projected[1] - py]
2928}
2929
2930/// Calculate some initial guesses for the given points,
2931/// which are being constrained to symmetric across the given line.
2932fn symmetric_hidden_point_guess(
2933    sketch_vars: &[KclValue],
2934    point: [SketchVarId; 2],
2935    axis: SymmetricLineVars,
2936    exec_state: &mut ExecState,
2937    range: crate::SourceRange,
2938) -> Result<[f64; 2], KclError> {
2939    let point = [
2940        sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
2941        sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
2942    ];
2943    let axis_start = [
2944        sketch_var_initial_value(sketch_vars, axis.start[0], exec_state, range)?,
2945        sketch_var_initial_value(sketch_vars, axis.start[1], exec_state, range)?,
2946    ];
2947    let axis_end = [
2948        sketch_var_initial_value(sketch_vars, axis.end[0], exec_state, range)?,
2949        sketch_var_initial_value(sketch_vars, axis.end[1], exec_state, range)?,
2950    ];
2951
2952    Ok(reflect_point_across_line(point, axis_start, axis_end))
2953}
2954
2955fn create_hidden_point(
2956    exec_state: &mut ExecState,
2957    initial_position: [f64; 2],
2958    range: crate::SourceRange,
2959) -> Result<[SketchVarId; 2], KclError> {
2960    let sketch_var_ty = solver_numeric_type(exec_state);
2961    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2962        return Err(KclError::new_semantic(KclErrorDetails::new(
2963            "symmetric() can only be used inside a sketch block".to_owned(),
2964            vec![range],
2965        )));
2966    };
2967
2968    let x_id = sketch_state.next_sketch_var_id();
2969    sketch_state.sketch_vars.push(KclValue::SketchVar {
2970        value: Box::new(crate::execution::SketchVar {
2971            id: x_id,
2972            initial_value: initial_position[0],
2973            ty: sketch_var_ty,
2974            meta: vec![],
2975        }),
2976    });
2977
2978    let y_id = sketch_state.next_sketch_var_id();
2979    sketch_state.sketch_vars.push(KclValue::SketchVar {
2980        value: Box::new(crate::execution::SketchVar {
2981            id: y_id,
2982            initial_value: initial_position[1],
2983            ty: sketch_var_ty,
2984            meta: vec![],
2985        }),
2986    });
2987
2988    Ok([x_id, y_id])
2989}
2990
2991pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2992    #[derive(Debug, Clone, Copy)]
2993    struct RadiusInputVars {
2994        center: [SketchVarId; 2],
2995        start: [SketchVarId; 2],
2996        end: Option<[SketchVarId; 2]>,
2997    }
2998
2999    #[derive(Debug, Clone, Copy)]
3000    enum EqualRadiusInput {
3001        Radius(RadiusInputVars),
3002    }
3003
3004    fn extract_equal_radius_input(
3005        segment_value: &KclValue,
3006        range: crate::SourceRange,
3007    ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
3008        let KclValue::Segment { value: segment } = segment_value else {
3009            return Err(KclError::new_semantic(KclErrorDetails::new(
3010                format!(
3011                    "equalRadius() arguments must be segments but found {}",
3012                    segment_value.human_friendly_type()
3013                ),
3014                vec![range],
3015            )));
3016        };
3017        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3018            return Err(KclError::new_semantic(KclErrorDetails::new(
3019                "equalRadius() arguments must be unsolved segments".to_owned(),
3020                vec![range],
3021            )));
3022        };
3023        match &unsolved.kind {
3024            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3025                let (
3026                    UnsolvedExpr::Unknown(center_x),
3027                    UnsolvedExpr::Unknown(center_y),
3028                    UnsolvedExpr::Unknown(start_x),
3029                    UnsolvedExpr::Unknown(start_y),
3030                    UnsolvedExpr::Unknown(end_x),
3031                    UnsolvedExpr::Unknown(end_y),
3032                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3033                else {
3034                    return Err(KclError::new_semantic(KclErrorDetails::new(
3035                        "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
3036                        vec![range],
3037                    )));
3038                };
3039                Ok((
3040                    EqualRadiusInput::Radius(RadiusInputVars {
3041                        center: [*center_x, *center_y],
3042                        start: [*start_x, *start_y],
3043                        end: Some([*end_x, *end_y]),
3044                    }),
3045                    unsolved.object_id,
3046                ))
3047            }
3048            UnsolvedSegmentKind::Circle { center, start, .. } => {
3049                let (
3050                    UnsolvedExpr::Unknown(center_x),
3051                    UnsolvedExpr::Unknown(center_y),
3052                    UnsolvedExpr::Unknown(start_x),
3053                    UnsolvedExpr::Unknown(start_y),
3054                ) = (&center[0], &center[1], &start[0], &start[1])
3055                else {
3056                    return Err(KclError::new_semantic(KclErrorDetails::new(
3057                        "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
3058                        vec![range],
3059                    )));
3060                };
3061                Ok((
3062                    EqualRadiusInput::Radius(RadiusInputVars {
3063                        center: [*center_x, *center_y],
3064                        start: [*start_x, *start_y],
3065                        end: None,
3066                    }),
3067                    unsolved.object_id,
3068                ))
3069            }
3070            other => Err(KclError::new_semantic(KclErrorDetails::new(
3071                format!(
3072                    "equalRadius() currently supports only arc and circle segments, you provided {}",
3073                    other.human_friendly_kind_with_article()
3074                ),
3075                vec![range],
3076            ))),
3077        }
3078    }
3079
3080    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3081        "input",
3082        &RuntimeType::Array(
3083            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3084            ArrayLen::Minimum(2),
3085        ),
3086        exec_state,
3087    )?;
3088    let range = args.source_range;
3089
3090    let extracted_input = input
3091        .iter()
3092        .map(|segment_value| extract_equal_radius_input(segment_value, range))
3093        .collect::<Result<Vec<_>, _>>()?;
3094    let radius_inputs: Vec<RadiusInputVars> = extracted_input
3095        .iter()
3096        .map(|(equal_radius_input, _)| match equal_radius_input {
3097            EqualRadiusInput::Radius(radius_input) => *radius_input,
3098        })
3099        .collect();
3100    #[cfg(feature = "artifact-graph")]
3101    let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
3102
3103    let sketch_var_ty = solver_numeric_type(exec_state);
3104    #[cfg(feature = "artifact-graph")]
3105    let constraint_id = exec_state.next_object_id();
3106
3107    let sketch_vars = {
3108        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3109            return Err(KclError::new_semantic(KclErrorDetails::new(
3110                "equalRadius() can only be used inside a sketch block".to_owned(),
3111                vec![range],
3112            )));
3113        };
3114        sketch_state.sketch_vars.clone()
3115    };
3116
3117    let radius_initial_value = radius_guess(
3118        &sketch_vars,
3119        radius_inputs[0].center,
3120        radius_inputs[0].start,
3121        exec_state,
3122        range,
3123    )?;
3124
3125    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3126        return Err(KclError::new_semantic(KclErrorDetails::new(
3127            "equalRadius() can only be used inside a sketch block".to_owned(),
3128            vec![range],
3129        )));
3130    };
3131    let radius_id = sketch_state.next_sketch_var_id();
3132    sketch_state.sketch_vars.push(KclValue::SketchVar {
3133        value: Box::new(crate::execution::SketchVar {
3134            id: radius_id,
3135            initial_value: radius_initial_value,
3136            ty: sketch_var_ty,
3137            meta: vec![],
3138        }),
3139    });
3140    let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3141
3142    for radius_input in radius_inputs {
3143        let center = datum_point(radius_input.center, range)?;
3144        let start = datum_point(radius_input.start, range)?;
3145        sketch_state
3146            .solver_constraints
3147            .push(SolverConstraint::DistanceVar(start, center, radius));
3148        if let Some(end) = radius_input.end {
3149            let end = datum_point(end, range)?;
3150            sketch_state
3151                .solver_constraints
3152                .push(SolverConstraint::DistanceVar(end, center, radius));
3153        }
3154    }
3155
3156    #[cfg(feature = "artifact-graph")]
3157    {
3158        let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
3159            input: input_object_ids,
3160        });
3161        sketch_state.sketch_constraints.push(constraint_id);
3162        track_constraint(constraint_id, constraint, exec_state, &args);
3163    }
3164
3165    Ok(KclValue::none())
3166}
3167
3168pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3169    let Some(Some(sketch_id)) = exec_state.sketch_block().map(|sb| sb.sketch_id) else {
3170        return Err(KclError::new_semantic(KclErrorDetails::new(
3171            "tangent() cannot be used outside a sketch block".to_owned(),
3172            vec![args.source_range],
3173        )));
3174    };
3175
3176    #[derive(Debug, Clone, Copy)]
3177    enum TangentInput {
3178        Line(LineVars),
3179        Circular(ArcVars),
3180    }
3181
3182    fn extract_tangent_input(
3183        segment_value: &KclValue,
3184        range: crate::SourceRange,
3185    ) -> Result<(TangentInput, ObjectId), KclError> {
3186        let KclValue::Segment { value: segment } = segment_value else {
3187            return Err(KclError::new_semantic(KclErrorDetails::new(
3188                "tangent() arguments must be segments".to_owned(),
3189                vec![range],
3190            )));
3191        };
3192        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3193            return Err(KclError::new_semantic(KclErrorDetails::new(
3194                "tangent() arguments must be unsolved segments".to_owned(),
3195                vec![range],
3196            )));
3197        };
3198        match &unsolved.kind {
3199            UnsolvedSegmentKind::Line { start, end, .. } => {
3200                let (
3201                    UnsolvedExpr::Unknown(start_x),
3202                    UnsolvedExpr::Unknown(start_y),
3203                    UnsolvedExpr::Unknown(end_x),
3204                    UnsolvedExpr::Unknown(end_y),
3205                ) = (&start[0], &start[1], &end[0], &end[1])
3206                else {
3207                    return Err(KclError::new_semantic(KclErrorDetails::new(
3208                        "line coordinates must be sketch vars for tangent()".to_owned(),
3209                        vec![range],
3210                    )));
3211                };
3212                Ok((
3213                    TangentInput::Line(LineVars {
3214                        start: [*start_x, *start_y],
3215                        end: [*end_x, *end_y],
3216                    }),
3217                    unsolved.object_id,
3218                ))
3219            }
3220            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3221                let (
3222                    UnsolvedExpr::Unknown(center_x),
3223                    UnsolvedExpr::Unknown(center_y),
3224                    UnsolvedExpr::Unknown(start_x),
3225                    UnsolvedExpr::Unknown(start_y),
3226                    UnsolvedExpr::Unknown(end_x),
3227                    UnsolvedExpr::Unknown(end_y),
3228                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3229                else {
3230                    return Err(KclError::new_semantic(KclErrorDetails::new(
3231                        "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
3232                        vec![range],
3233                    )));
3234                };
3235                Ok((
3236                    TangentInput::Circular(ArcVars {
3237                        center: [*center_x, *center_y],
3238                        start: [*start_x, *start_y],
3239                        end: Some([*end_x, *end_y]),
3240                    }),
3241                    unsolved.object_id,
3242                ))
3243            }
3244            UnsolvedSegmentKind::Circle { center, start, .. } => {
3245                let (
3246                    UnsolvedExpr::Unknown(center_x),
3247                    UnsolvedExpr::Unknown(center_y),
3248                    UnsolvedExpr::Unknown(start_x),
3249                    UnsolvedExpr::Unknown(start_y),
3250                ) = (&center[0], &center[1], &start[0], &start[1])
3251                else {
3252                    return Err(KclError::new_semantic(KclErrorDetails::new(
3253                        "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
3254                        vec![range],
3255                    )));
3256                };
3257                Ok((
3258                    TangentInput::Circular(ArcVars {
3259                        center: [*center_x, *center_y],
3260                        start: [*start_x, *start_y],
3261                        end: None,
3262                    }),
3263                    unsolved.object_id,
3264                ))
3265            }
3266            _ => Err(KclError::new_semantic(KclErrorDetails::new(
3267                "tangent() supports only line, arc, and circle segments".to_owned(),
3268                vec![range],
3269            ))),
3270        }
3271    }
3272
3273    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3274        "input",
3275        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3276        exec_state,
3277    )?;
3278    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
3279        KclError::new_semantic(KclErrorDetails::new(
3280            "tangent() requires exactly 2 input segments".to_owned(),
3281            vec![args.source_range],
3282        ))
3283    })?;
3284    let range = args.source_range;
3285    let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
3286    let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
3287    #[cfg(not(feature = "artifact-graph"))]
3288    let _ = (input0_object_id, input1_object_id);
3289
3290    enum TangentCase {
3291        LineCircular(LineVars, ArcVars),
3292        CircularCircular(ArcVars, ArcVars),
3293    }
3294    let tangent_case = match (input0, input1) {
3295        (TangentInput::Line(line), TangentInput::Circular(circular))
3296        | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
3297        (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
3298            TangentCase::CircularCircular(circular0, circular1)
3299        }
3300        (TangentInput::Line(_), TangentInput::Line(_)) => {
3301            return Err(KclError::new_semantic(KclErrorDetails::new(
3302                "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
3303                vec![range],
3304            )));
3305        }
3306    };
3307
3308    let sketch_var_ty = solver_numeric_type(exec_state);
3309    #[cfg(feature = "artifact-graph")]
3310    let constraint_id = exec_state.next_object_id();
3311
3312    let sketch_vars = {
3313        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3314            return Err(KclError::new_semantic(KclErrorDetails::new(
3315                "tangent() can only be used inside a sketch block".to_owned(),
3316                vec![range],
3317            )));
3318        };
3319        sketch_state.sketch_vars.clone()
3320    };
3321
3322    // Hidden radius vars. Empty metadata keeps them out of source write-back.
3323    match tangent_case {
3324        TangentCase::LineCircular(line, circular) => {
3325            let tangency_key = make_line_arc_tangency_key(line, circular);
3326            let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
3327                Some(ConstraintState::Tangency(TangencyMode::LineCircle(side))) => side,
3328                _ => {
3329                    let side = infer_line_tangent_side(&sketch_vars, line, circular.center, exec_state, range)?;
3330                    exec_state.set_constraint_state(
3331                        sketch_id,
3332                        tangency_key,
3333                        ConstraintState::Tangency(TangencyMode::LineCircle(side)),
3334                    );
3335                    side
3336                }
3337            };
3338            let line_p0 = datum_point(line.start, range)?;
3339            let line_p1 = datum_point(line.end, range)?;
3340            let line_datum = DatumLineSegment::new(line_p0, line_p1);
3341
3342            let center = datum_point(circular.center, range)?;
3343            let circular_start = datum_point(circular.start, range)?;
3344            let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
3345            let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
3346            let Some(sketch_state) = exec_state.sketch_block_mut() else {
3347                return Err(KclError::new_semantic(KclErrorDetails::new(
3348                    "tangent() can only be used inside a sketch block".to_owned(),
3349                    vec![range],
3350                )));
3351            };
3352            let radius_id = sketch_state.next_sketch_var_id();
3353            sketch_state.sketch_vars.push(KclValue::SketchVar {
3354                value: Box::new(crate::execution::SketchVar {
3355                    id: radius_id,
3356                    initial_value: radius_initial_value,
3357                    ty: sketch_var_ty,
3358                    meta: vec![],
3359                }),
3360            });
3361            let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3362            let circle = DatumCircle { center, radius };
3363
3364            // Tangency decomposition for Line/circular segment:
3365            // 1) Introduce a hidden radius variable r for the segment's underlying circle.
3366            // 2) Keep the segment's defining points on that circle with DistanceVar(point, center, r).
3367            // 3) Apply the native LineTangentToCircle solver constraint.
3368            sketch_state
3369                .solver_constraints
3370                .push(SolverConstraint::DistanceVar(circular_start, center, radius));
3371            if let Some(circular_end) = circular_end {
3372                sketch_state
3373                    .solver_constraints
3374                    .push(SolverConstraint::DistanceVar(circular_end, center, radius));
3375            }
3376            sketch_state
3377                .solver_constraints
3378                .push(SolverConstraint::LineTangentToCircle(line_datum, circle, tangency_side));
3379        }
3380        TangentCase::CircularCircular(circular0, circular1) => {
3381            let tangency_key = make_arc_arc_tangency_key(circular0, circular1);
3382            let tangency_side = match exec_state.constraint_state(sketch_id, &tangency_key) {
3383                Some(ConstraintState::Tangency(TangencyMode::CircleCircle(side))) => side,
3384                _ => {
3385                    let side = infer_arc_tangent_side(&sketch_vars, circular0, circular1, exec_state, range)?;
3386                    exec_state.set_constraint_state(
3387                        sketch_id,
3388                        tangency_key,
3389                        ConstraintState::Tangency(TangencyMode::CircleCircle(side)),
3390                    );
3391                    side
3392                }
3393            };
3394            let center0 = datum_point(circular0.center, range)?;
3395            let start0 = datum_point(circular0.start, range)?;
3396            let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
3397            let radius0_initial_value =
3398                radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
3399            let center1 = datum_point(circular1.center, range)?;
3400            let start1 = datum_point(circular1.start, range)?;
3401            let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
3402            let radius1_initial_value =
3403                radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
3404            let Some(sketch_state) = exec_state.sketch_block_mut() else {
3405                return Err(KclError::new_semantic(KclErrorDetails::new(
3406                    "tangent() can only be used inside a sketch block".to_owned(),
3407                    vec![range],
3408                )));
3409            };
3410            let radius0_id = sketch_state.next_sketch_var_id();
3411            sketch_state.sketch_vars.push(KclValue::SketchVar {
3412                value: Box::new(crate::execution::SketchVar {
3413                    id: radius0_id,
3414                    initial_value: radius0_initial_value,
3415                    ty: sketch_var_ty,
3416                    meta: vec![],
3417                }),
3418            });
3419            let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
3420            let circle0 = DatumCircle {
3421                center: center0,
3422                radius: radius0,
3423            };
3424
3425            let radius1_id = sketch_state.next_sketch_var_id();
3426            sketch_state.sketch_vars.push(KclValue::SketchVar {
3427                value: Box::new(crate::execution::SketchVar {
3428                    id: radius1_id,
3429                    initial_value: radius1_initial_value,
3430                    ty: sketch_var_ty,
3431                    meta: vec![],
3432                }),
3433            });
3434            let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
3435            let circle1 = DatumCircle {
3436                center: center1,
3437                radius: radius1,
3438            };
3439
3440            // Tangency decomposition for circular segment/circular segment:
3441            // 1) Introduce one hidden radius variable per arc.
3442            // 2) Keep each segment's defining points on its corresponding circle.
3443            // 3) Apply the native CircleTangentToCircle solver constraint.
3444            sketch_state
3445                .solver_constraints
3446                .push(SolverConstraint::DistanceVar(start0, center0, radius0));
3447            if let Some(end0) = end0 {
3448                sketch_state
3449                    .solver_constraints
3450                    .push(SolverConstraint::DistanceVar(end0, center0, radius0));
3451            }
3452            sketch_state
3453                .solver_constraints
3454                .push(SolverConstraint::DistanceVar(start1, center1, radius1));
3455            if let Some(end1) = end1 {
3456                sketch_state
3457                    .solver_constraints
3458                    .push(SolverConstraint::DistanceVar(end1, center1, radius1));
3459            }
3460            sketch_state
3461                .solver_constraints
3462                .push(SolverConstraint::CircleTangentToCircle(circle0, circle1, tangency_side));
3463        }
3464    }
3465
3466    #[cfg(feature = "artifact-graph")]
3467    {
3468        let constraint = crate::front::Constraint::Tangent(Tangent {
3469            input: vec![input0_object_id, input1_object_id],
3470        });
3471        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3472            return Err(KclError::new_semantic(KclErrorDetails::new(
3473                "tangent() can only be used inside a sketch block".to_owned(),
3474                vec![range],
3475            )));
3476        };
3477        sketch_state.sketch_constraints.push(constraint_id);
3478        track_constraint(constraint_id, constraint, exec_state, &args);
3479    }
3480
3481    Ok(KclValue::none())
3482}
3483
3484#[derive(Debug, Clone, Copy)]
3485struct SymmetricPointVars {
3486    coords: [SketchVarId; 2],
3487    #[cfg(feature = "artifact-graph")]
3488    object_id: ObjectId,
3489}
3490
3491/// The line that geometry should be symmetric across.
3492#[derive(Debug, Clone, Copy)]
3493struct SymmetricLineVars {
3494    start: [SketchVarId; 2],
3495    end: [SketchVarId; 2],
3496    #[cfg(feature = "artifact-graph")]
3497    object_id: ObjectId,
3498}
3499
3500#[derive(Debug, Clone, Copy)]
3501struct SymmetricArcVars {
3502    center: [SketchVarId; 2],
3503    start: [SketchVarId; 2],
3504    end: [SketchVarId; 2],
3505    #[cfg(feature = "artifact-graph")]
3506    object_id: ObjectId,
3507}
3508
3509#[derive(Debug, Clone, Copy)]
3510struct SymmetricCircleVars {
3511    center: [SketchVarId; 2],
3512    start: [SketchVarId; 2],
3513    #[cfg(feature = "artifact-graph")]
3514    object_id: ObjectId,
3515}
3516
3517#[derive(Debug, Clone, Copy)]
3518enum SymmetricInput {
3519    Point(SymmetricPointVars),
3520    Line(SymmetricLineVars),
3521    Arc(SymmetricArcVars),
3522    Circle(SymmetricCircleVars),
3523}
3524
3525impl SymmetricInput {
3526    fn type_name(self) -> &'static str {
3527        match self {
3528            SymmetricInput::Point(_) => "points",
3529            SymmetricInput::Line(_) => "lines",
3530            SymmetricInput::Arc(_) => "arcs",
3531            SymmetricInput::Circle(_) => "circles",
3532        }
3533    }
3534
3535    #[cfg(feature = "artifact-graph")]
3536    fn object_id(self) -> ObjectId {
3537        match self {
3538            SymmetricInput::Point(point) => point.object_id,
3539            SymmetricInput::Line(line) => line.object_id,
3540            SymmetricInput::Arc(arc) => arc.object_id,
3541            SymmetricInput::Circle(circle) => circle.object_id,
3542        }
3543    }
3544}
3545
3546fn extract_symmetric_input(segment_value: &KclValue, range: crate::SourceRange) -> Result<SymmetricInput, KclError> {
3547    let KclValue::Segment { value: segment } = segment_value else {
3548        return Err(KclError::new_semantic(KclErrorDetails::new(
3549            format!(
3550                "symmetric() arguments must be point, line, arc, or circle segments, but found {}",
3551                segment_value.human_friendly_type()
3552            ),
3553            vec![range],
3554        )));
3555    };
3556    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3557        return Err(KclError::new_semantic(KclErrorDetails::new(
3558            "symmetric() arguments must be unsolved segments".to_owned(),
3559            vec![range],
3560        )));
3561    };
3562
3563    match &unsolved.kind {
3564        UnsolvedSegmentKind::Point { position, .. } => {
3565            let (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) = (&position[0], &position[1]) else {
3566                return Err(KclError::new_semantic(KclErrorDetails::new(
3567                    "point coordinates must be sketch vars for symmetric()".to_owned(),
3568                    vec![range],
3569                )));
3570            };
3571            Ok(SymmetricInput::Point(SymmetricPointVars {
3572                coords: [*x, *y],
3573                #[cfg(feature = "artifact-graph")]
3574                object_id: unsolved.object_id,
3575            }))
3576        }
3577        UnsolvedSegmentKind::Line { start, end, .. } => {
3578            let (
3579                UnsolvedExpr::Unknown(start_x),
3580                UnsolvedExpr::Unknown(start_y),
3581                UnsolvedExpr::Unknown(end_x),
3582                UnsolvedExpr::Unknown(end_y),
3583            ) = (&start[0], &start[1], &end[0], &end[1])
3584            else {
3585                return Err(KclError::new_semantic(KclErrorDetails::new(
3586                    "line coordinates must be sketch vars for symmetric()".to_owned(),
3587                    vec![range],
3588                )));
3589            };
3590            Ok(SymmetricInput::Line(SymmetricLineVars {
3591                start: [*start_x, *start_y],
3592                end: [*end_x, *end_y],
3593                #[cfg(feature = "artifact-graph")]
3594                object_id: unsolved.object_id,
3595            }))
3596        }
3597        UnsolvedSegmentKind::Arc { center, start, end, .. } => {
3598            let (
3599                UnsolvedExpr::Unknown(center_x),
3600                UnsolvedExpr::Unknown(center_y),
3601                UnsolvedExpr::Unknown(start_x),
3602                UnsolvedExpr::Unknown(start_y),
3603                UnsolvedExpr::Unknown(end_x),
3604                UnsolvedExpr::Unknown(end_y),
3605            ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
3606            else {
3607                return Err(KclError::new_semantic(KclErrorDetails::new(
3608                    "arc center/start/end coordinates must be sketch vars for symmetric()".to_owned(),
3609                    vec![range],
3610                )));
3611            };
3612            Ok(SymmetricInput::Arc(SymmetricArcVars {
3613                center: [*center_x, *center_y],
3614                start: [*start_x, *start_y],
3615                end: [*end_x, *end_y],
3616                #[cfg(feature = "artifact-graph")]
3617                object_id: unsolved.object_id,
3618            }))
3619        }
3620        UnsolvedSegmentKind::Circle { center, start, .. } => {
3621            let (
3622                UnsolvedExpr::Unknown(center_x),
3623                UnsolvedExpr::Unknown(center_y),
3624                UnsolvedExpr::Unknown(start_x),
3625                UnsolvedExpr::Unknown(start_y),
3626            ) = (&center[0], &center[1], &start[0], &start[1])
3627            else {
3628                return Err(KclError::new_semantic(KclErrorDetails::new(
3629                    "circle center/start coordinates must be sketch vars for symmetric()".to_owned(),
3630                    vec![range],
3631                )));
3632            };
3633            Ok(SymmetricInput::Circle(SymmetricCircleVars {
3634                center: [*center_x, *center_y],
3635                start: [*start_x, *start_y],
3636                #[cfg(feature = "artifact-graph")]
3637                object_id: unsolved.object_id,
3638            }))
3639        }
3640    }
3641}
3642
3643fn extract_symmetric_axis_line(
3644    segment_value: &KclValue,
3645    range: crate::SourceRange,
3646) -> Result<SymmetricLineVars, KclError> {
3647    let KclValue::Segment { value: segment } = segment_value else {
3648        return Err(KclError::new_semantic(KclErrorDetails::new(
3649            format!(
3650                "symmetric() axis must be a line Segment, but found {}",
3651                segment_value.human_friendly_type()
3652            ),
3653            vec![range],
3654        )));
3655    };
3656    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3657        return Err(KclError::new_semantic(KclErrorDetails::new(
3658            "symmetric() axis must be an unsolved line Segment".to_owned(),
3659            vec![range],
3660        )));
3661    };
3662    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3663        return Err(KclError::new_semantic(KclErrorDetails::new(
3664            "symmetric() axis must be a line Segment".to_owned(),
3665            vec![range],
3666        )));
3667    };
3668    let (
3669        UnsolvedExpr::Unknown(start_x),
3670        UnsolvedExpr::Unknown(start_y),
3671        UnsolvedExpr::Unknown(end_x),
3672        UnsolvedExpr::Unknown(end_y),
3673    ) = (&start[0], &start[1], &end[0], &end[1])
3674    else {
3675        return Err(KclError::new_semantic(KclErrorDetails::new(
3676            "symmetric() axis line coordinates must be sketch vars".to_owned(),
3677            vec![range],
3678        )));
3679    };
3680
3681    Ok(SymmetricLineVars {
3682        start: [*start_x, *start_y],
3683        end: [*end_x, *end_y],
3684        #[cfg(feature = "artifact-graph")]
3685        object_id: unsolved.object_id,
3686    })
3687}
3688
3689pub async fn symmetric(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3690    #[derive(Debug, Clone, Copy)]
3691    struct SymmetricCircularVars {
3692        center: [SketchVarId; 2],
3693        start: [SketchVarId; 2],
3694        end: Option<[SketchVarId; 2]>,
3695    }
3696
3697    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
3698        "input",
3699        &RuntimeType::Array(
3700            Box::new(RuntimeType::Primitive(PrimitiveType::Segment)),
3701            ArrayLen::Known(2),
3702        ),
3703        exec_state,
3704    )?;
3705    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
3706        KclError::new_semantic(KclErrorDetails::new(
3707            "symmetric() requires exactly 2 input segments".to_owned(),
3708            vec![args.source_range],
3709        ))
3710    })?;
3711    let axis: KclValue = args.get_kw_arg("axis", &RuntimeType::Primitive(PrimitiveType::Segment), exec_state)?;
3712    let range = args.source_range;
3713
3714    let input0 = extract_symmetric_input(&item0, range)?;
3715    let input1 = extract_symmetric_input(&item1, range)?;
3716    let axis_line = extract_symmetric_axis_line(&axis, range)?;
3717
3718    let solver_axis = DatumLineSegment::new(datum_point(axis_line.start, range)?, datum_point(axis_line.end, range)?);
3719
3720    let (mut solver_constraints, circular_inputs) = match (input0, input1) {
3721        (SymmetricInput::Point(point0), SymmetricInput::Point(point1)) => (
3722            vec![SolverConstraint::Symmetric(
3723                solver_axis,
3724                datum_point(point0.coords, range)?,
3725                datum_point(point1.coords, range)?,
3726            )],
3727            None,
3728        ),
3729        (SymmetricInput::Line(line0), SymmetricInput::Line(line1)) => {
3730            let sketch_vars = {
3731                let Some(sketch_state) = exec_state.sketch_block_mut() else {
3732                    return Err(KclError::new_semantic(KclErrorDetails::new(
3733                        "symmetric() can only be used inside a sketch block".to_owned(),
3734                        vec![range],
3735                    )));
3736                };
3737                sketch_state.sketch_vars.clone()
3738            };
3739            let mirrored_start = symmetric_hidden_point_guess(&sketch_vars, line0.start, axis_line, exec_state, range)?;
3740            let mirrored_end = symmetric_hidden_point_guess(&sketch_vars, line0.end, axis_line, exec_state, range)?;
3741            let hidden_start = create_hidden_point(exec_state, mirrored_start, range)?;
3742            let hidden_end = create_hidden_point(exec_state, mirrored_end, range)?;
3743            let mirrored_support_line =
3744                DatumLineSegment::new(datum_point(hidden_start, range)?, datum_point(hidden_end, range)?);
3745            let solver_line1 = DatumLineSegment::new(datum_point(line1.start, range)?, datum_point(line1.end, range)?);
3746
3747            (
3748                vec![
3749                    SolverConstraint::Symmetric(
3750                        solver_axis,
3751                        datum_point(line0.start, range)?,
3752                        datum_point(hidden_start, range)?,
3753                    ),
3754                    SolverConstraint::Symmetric(
3755                        solver_axis,
3756                        datum_point(line0.end, range)?,
3757                        datum_point(hidden_end, range)?,
3758                    ),
3759                    SolverConstraint::LinesAtAngle(mirrored_support_line, solver_line1, AngleKind::Parallel),
3760                    // Keep the second segment on the mirrored support line without
3761                    // forcing its endpoints to be pairwise mirrored.
3762                    SolverConstraint::PointLineDistance(datum_point(line1.start, range)?, mirrored_support_line, 0.0),
3763                ],
3764                None,
3765            )
3766        }
3767        (SymmetricInput::Arc(arc0), SymmetricInput::Arc(arc1)) => (
3768            vec![SolverConstraint::Symmetric(
3769                solver_axis,
3770                datum_point(arc0.center, range)?,
3771                datum_point(arc1.center, range)?,
3772            )],
3773            Some([
3774                SymmetricCircularVars {
3775                    center: arc0.center,
3776                    start: arc0.start,
3777                    end: Some(arc0.end),
3778                },
3779                SymmetricCircularVars {
3780                    center: arc1.center,
3781                    start: arc1.start,
3782                    end: Some(arc1.end),
3783                },
3784            ]),
3785        ),
3786        (SymmetricInput::Circle(circle0), SymmetricInput::Circle(circle1)) => (
3787            vec![SolverConstraint::Symmetric(
3788                solver_axis,
3789                datum_point(circle0.center, range)?,
3790                datum_point(circle1.center, range)?,
3791            )],
3792            Some([
3793                SymmetricCircularVars {
3794                    center: circle0.center,
3795                    start: circle0.start,
3796                    end: None,
3797                },
3798                SymmetricCircularVars {
3799                    center: circle1.center,
3800                    start: circle1.start,
3801                    end: None,
3802                },
3803            ]),
3804        ),
3805        _ => {
3806            return Err(KclError::new_semantic(KclErrorDetails::new(
3807                format!(
3808                    "symmetric() inputs must be homogeneous. You provided {} and {}",
3809                    input0.type_name(),
3810                    input1.type_name()
3811                ),
3812                vec![range],
3813            )));
3814        }
3815    };
3816
3817    if let Some([circular0, circular1]) = circular_inputs {
3818        let sketch_var_ty = solver_numeric_type(exec_state);
3819        let sketch_vars = {
3820            let Some(sketch_state) = exec_state.sketch_block_mut() else {
3821                return Err(KclError::new_semantic(KclErrorDetails::new(
3822                    "symmetric() can only be used inside a sketch block".to_owned(),
3823                    vec![range],
3824                )));
3825            };
3826            sketch_state.sketch_vars.clone()
3827        };
3828        let radius_initial_value = radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
3829
3830        let Some(sketch_state) = exec_state.sketch_block_mut() else {
3831            return Err(KclError::new_semantic(KclErrorDetails::new(
3832                "symmetric() can only be used inside a sketch block".to_owned(),
3833                vec![range],
3834            )));
3835        };
3836        let radius_id = sketch_state.next_sketch_var_id();
3837        sketch_state.sketch_vars.push(KclValue::SketchVar {
3838            value: Box::new(crate::execution::SketchVar {
3839                id: radius_id,
3840                initial_value: radius_initial_value,
3841                ty: sketch_var_ty,
3842                meta: vec![],
3843            }),
3844        });
3845        let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
3846
3847        for circular in [circular0, circular1] {
3848            let center = datum_point(circular.center, range)?;
3849            let start = datum_point(circular.start, range)?;
3850            solver_constraints.push(SolverConstraint::DistanceVar(start, center, radius));
3851            if let Some(end) = circular.end {
3852                let end = datum_point(end, range)?;
3853                solver_constraints.push(SolverConstraint::DistanceVar(end, center, radius));
3854            }
3855        }
3856    }
3857
3858    #[cfg(feature = "artifact-graph")]
3859    let constraint_id = exec_state.next_object_id();
3860    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3861        return Err(KclError::new_semantic(KclErrorDetails::new(
3862            "symmetric() can only be used inside a sketch block".to_owned(),
3863            vec![range],
3864        )));
3865    };
3866    sketch_state.solver_constraints.extend(solver_constraints);
3867
3868    #[cfg(feature = "artifact-graph")]
3869    {
3870        let constraint = crate::front::Constraint::Symmetric(Symmetric {
3871            input: vec![input0.object_id(), input1.object_id()],
3872            axis: axis_line.object_id,
3873        });
3874        sketch_state.sketch_constraints.push(constraint_id);
3875        track_constraint(constraint_id, constraint, exec_state, &args);
3876    }
3877
3878    Ok(KclValue::none())
3879}
3880
3881#[derive(Debug, Clone, Copy)]
3882pub(crate) enum LinesAtAngleKind {
3883    Parallel,
3884    Perpendicular,
3885}
3886
3887impl LinesAtAngleKind {
3888    pub fn to_function_name(self) -> &'static str {
3889        match self {
3890            LinesAtAngleKind::Parallel => "parallel",
3891            LinesAtAngleKind::Perpendicular => "perpendicular",
3892        }
3893    }
3894
3895    fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
3896        match self {
3897            LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
3898            LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
3899        }
3900    }
3901
3902    #[cfg(feature = "artifact-graph")]
3903    fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
3904        match self {
3905            LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
3906            LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
3907        }
3908    }
3909}
3910
3911/// Convert between two different libraries with similar angle representations
3912#[expect(unused)]
3913fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
3914    kcmc::shared::Angle::from_degrees(angle.to_degrees())
3915}
3916
3917/// Convert between two different libraries with similar angle representations
3918#[expect(unused)]
3919fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
3920    ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
3921}
3922
3923pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3924    #[derive(Clone, Copy)]
3925    struct ConstrainableLine {
3926        solver_line: DatumLineSegment,
3927        #[cfg(feature = "artifact-graph")]
3928        object_id: ObjectId,
3929    }
3930
3931    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3932        "lines",
3933        &RuntimeType::Array(
3934            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
3935            ArrayLen::Minimum(2),
3936        ),
3937        exec_state,
3938    )?;
3939    let range = args.source_range;
3940    let constrainable_lines: Vec<ConstrainableLine> = lines
3941        .iter()
3942        .map(|line| {
3943            let KclValue::Segment { value: segment } = line else {
3944                return Err(KclError::new_semantic(KclErrorDetails::new(
3945                    "line argument must be a Segment".to_owned(),
3946                    vec![args.source_range],
3947                )));
3948            };
3949            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3950                return Err(KclError::new_internal(KclErrorDetails::new(
3951                    "line must be an unsolved Segment".to_owned(),
3952                    vec![args.source_range],
3953                )));
3954            };
3955            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3956                return Err(KclError::new_semantic(KclErrorDetails::new(
3957                    "line argument must be a line, no other type of Segment".to_owned(),
3958                    vec![args.source_range],
3959                )));
3960            };
3961            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
3962                return Err(KclError::new_semantic(KclErrorDetails::new(
3963                    "line's start x coordinate must be a var".to_owned(),
3964                    vec![args.source_range],
3965                )));
3966            };
3967            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
3968                return Err(KclError::new_semantic(KclErrorDetails::new(
3969                    "line's start y coordinate must be a var".to_owned(),
3970                    vec![args.source_range],
3971                )));
3972            };
3973            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
3974                return Err(KclError::new_semantic(KclErrorDetails::new(
3975                    "line's end x coordinate must be a var".to_owned(),
3976                    vec![args.source_range],
3977                )));
3978            };
3979            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
3980                return Err(KclError::new_semantic(KclErrorDetails::new(
3981                    "line's end y coordinate must be a var".to_owned(),
3982                    vec![args.source_range],
3983                )));
3984            };
3985
3986            let solver_line_p0 =
3987                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
3988            let solver_line_p1 =
3989                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
3990
3991            Ok(ConstrainableLine {
3992                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
3993                #[cfg(feature = "artifact-graph")]
3994                object_id: unsolved.object_id,
3995            })
3996        })
3997        .collect::<Result<_, _>>()?;
3998
3999    #[cfg(feature = "artifact-graph")]
4000    let constraint_id = exec_state.next_object_id();
4001    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4002        return Err(KclError::new_semantic(KclErrorDetails::new(
4003            "parallel() can only be used inside a sketch block".to_owned(),
4004            vec![args.source_range],
4005        )));
4006    };
4007
4008    let n = constrainable_lines.len();
4009    let mut constrainable_lines_iter = constrainable_lines.iter();
4010    let first_line = constrainable_lines_iter
4011        .next()
4012        .ok_or(KclError::new_semantic(KclErrorDetails::new(
4013            format!("parallel() requires at least 2 lines, but you provided {}", n),
4014            vec![args.source_range],
4015        )))?;
4016    for line in constrainable_lines_iter {
4017        sketch_state.solver_constraints.push(SolverConstraint::LinesAtAngle(
4018            first_line.solver_line,
4019            line.solver_line,
4020            AngleKind::Parallel,
4021        ));
4022    }
4023    #[cfg(feature = "artifact-graph")]
4024    {
4025        let constraint = Constraint::Parallel(Parallel {
4026            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
4027        });
4028        sketch_state.sketch_constraints.push(constraint_id);
4029        track_constraint(constraint_id, constraint, exec_state, &args);
4030    }
4031    Ok(KclValue::none())
4032}
4033
4034pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4035    lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
4036}
4037
4038/// A way to constrain points, or a line.
4039#[derive(Debug, Clone, Copy)]
4040enum AxisConstraintKind {
4041    Horizontal,
4042    Vertical,
4043}
4044
4045impl AxisConstraintKind {
4046    /// Which KCL function this corresponds to.
4047    fn function_name(self) -> &'static str {
4048        match self {
4049            AxisConstraintKind::Horizontal => "horizontal",
4050            AxisConstraintKind::Vertical => "vertical",
4051        }
4052    }
4053
4054    /// Use this constraint to align a line.
4055    fn line_constraint(self, line: DatumLineSegment) -> SolverConstraint {
4056        match self {
4057            AxisConstraintKind::Horizontal => SolverConstraint::Horizontal(line),
4058            AxisConstraintKind::Vertical => SolverConstraint::Vertical(line),
4059        }
4060    }
4061
4062    /// Use this constraint to align a pair of points.
4063    fn point_pair_constraint(self, p0: DatumPoint, p1: DatumPoint) -> SolverConstraint {
4064        match self {
4065            // A horizontal point set means all Y values are equal.
4066            AxisConstraintKind::Horizontal => SolverConstraint::VerticalDistance(p1, p0, 0.0),
4067            // A vertical point set means all X values are equal.
4068            AxisConstraintKind::Vertical => SolverConstraint::HorizontalDistance(p1, p0, 0.0),
4069        }
4070    }
4071
4072    /// Use this constraint to align a point to some known X or Y.
4073    fn constraint_aligning_point_to_constant(self, p0: DatumPoint, fixed_point: (f64, f64)) -> SolverConstraint {
4074        match self {
4075            AxisConstraintKind::Horizontal => SolverConstraint::Fixed(p0.y_id, fixed_point.1),
4076            AxisConstraintKind::Vertical => SolverConstraint::Fixed(p0.x_id, fixed_point.0),
4077        }
4078    }
4079
4080    #[cfg(feature = "artifact-graph")]
4081    fn line_artifact_constraint(self, line: ObjectId) -> Constraint {
4082        match self {
4083            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Line { line }),
4084            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Line { line }),
4085        }
4086    }
4087
4088    #[cfg(feature = "artifact-graph")]
4089    fn point_artifact_constraint(self, points: Vec<ConstraintSegment>) -> Constraint {
4090        match self {
4091            AxisConstraintKind::Horizontal => Constraint::Horizontal(Horizontal::Points { points }),
4092            AxisConstraintKind::Vertical => Constraint::Vertical(Vertical::Points { points }),
4093        }
4094    }
4095}
4096
4097/// The line the user wants to align vertically/horizontally.
4098/// Extracted from KCL arguments.
4099#[derive(Debug, Clone, Copy)]
4100struct AxisLineVars {
4101    start: [SketchVarId; 2],
4102    end: [SketchVarId; 2],
4103    #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
4104    object_id: ObjectId,
4105}
4106
4107fn extract_axis_line_vars(
4108    segment: &AbstractSegment,
4109    kind: AxisConstraintKind,
4110    source_range: crate::SourceRange,
4111) -> Result<AxisLineVars, KclError> {
4112    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4113        return Err(KclError::new_internal(KclErrorDetails::new(
4114            "line must be an unsolved Segment".to_owned(),
4115            vec![source_range],
4116        )));
4117    };
4118    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
4119        return Err(KclError::new_semantic(KclErrorDetails::new(
4120            format!(
4121                "{}() line argument must be a line, no other type of Segment",
4122                kind.function_name()
4123            ),
4124            vec![source_range],
4125        )));
4126    };
4127    let (
4128        UnsolvedExpr::Unknown(start_x),
4129        UnsolvedExpr::Unknown(start_y),
4130        UnsolvedExpr::Unknown(end_x),
4131        UnsolvedExpr::Unknown(end_y),
4132    ) = (&start[0], &start[1], &end[0], &end[1])
4133    else {
4134        return Err(KclError::new_semantic(KclErrorDetails::new(
4135            "line's x and y coordinates of both start and end must be vars".to_owned(),
4136            vec![source_range],
4137        )));
4138    };
4139
4140    Ok(AxisLineVars {
4141        start: [*start_x, *start_y],
4142        end: [*end_x, *end_y],
4143        object_id: unsolved.object_id,
4144    })
4145}
4146
4147#[derive(Debug, Clone)]
4148enum PointToAlign {
4149    /// Variable point that could be constrained.
4150    Variable { x: SketchVarId, y: SketchVarId },
4151    /// Fixed millimeter constant.
4152    Fixed { x: TyF64, y: TyF64 },
4153}
4154
4155impl From<[SketchVarId; 2]> for PointToAlign {
4156    fn from(sketch_var: [SketchVarId; 2]) -> Self {
4157        Self::Variable {
4158            x: sketch_var[0],
4159            y: sketch_var[1],
4160        }
4161    }
4162}
4163
4164impl From<[TyF64; 2]> for PointToAlign {
4165    fn from([x, y]: [TyF64; 2]) -> Self {
4166        Self::Fixed { x, y }
4167    }
4168}
4169
4170fn extract_axis_point_vars(
4171    input: &KclValue,
4172    kind: AxisConstraintKind,
4173    source_range: crate::SourceRange,
4174) -> Result<PointToAlign, KclError> {
4175    match input {
4176        KclValue::Segment { value: segment } => {
4177            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4178                return Err(KclError::new_semantic(KclErrorDetails::new(
4179                    format!(
4180                        "The `{}` function point arguments must be unsolved points",
4181                        kind.function_name()
4182                    ),
4183                    vec![source_range],
4184                )));
4185            };
4186            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
4187                return Err(KclError::new_semantic(KclErrorDetails::new(
4188                    format!(
4189                        "The `{}` function list arguments must be points, but one item is {}",
4190                        kind.function_name(),
4191                        unsolved.kind.human_friendly_kind_with_article()
4192                    ),
4193                    vec![source_range],
4194                )));
4195            };
4196            match (&position[0], &position[1]) {
4197                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed {
4198                    x: x.to_owned(),
4199                    y: y.to_owned(),
4200                }),
4201                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x: *x, y: *y }),
4202                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4203                    Err(KclError::new_semantic(KclErrorDetails::new(
4204                        format!(
4205                            "The `{}` function cannot take a fixed X component and a variable Y component",
4206                            kind.function_name()
4207                        ),
4208                        vec![source_range],
4209                    )))
4210                }
4211                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4212                    Err(KclError::new_semantic(KclErrorDetails::new(
4213                        format!(
4214                            "The `{}` function cannot take a fixed X component and a variable Y component",
4215                            kind.function_name()
4216                        ),
4217                        vec![source_range],
4218                    )))
4219                }
4220            }
4221        }
4222        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4223            let [x_value, y_value] = value.as_slice() else {
4224                return Err(KclError::new_semantic(KclErrorDetails::new(
4225                    format!(
4226                        "The `{}` function point arguments must each be a Point2d like [var 0mm, var 0mm]",
4227                        kind.function_name()
4228                    ),
4229                    vec![source_range],
4230                )));
4231            };
4232            let Some(x_expr) = x_value.as_unsolved_expr() else {
4233                return Err(KclError::new_semantic(KclErrorDetails::new(
4234                    format!(
4235                        "The `{}` function point x coordinate must be a number or sketch var",
4236                        kind.function_name()
4237                    ),
4238                    vec![source_range],
4239                )));
4240            };
4241            let Some(y_expr) = y_value.as_unsolved_expr() else {
4242                return Err(KclError::new_semantic(KclErrorDetails::new(
4243                    format!(
4244                        "The `{}` function point y coordinate must be a number or sketch var",
4245                        kind.function_name()
4246                    ),
4247                    vec![source_range],
4248                )));
4249            };
4250            match (x_expr, y_expr) {
4251                (UnsolvedExpr::Known(x), UnsolvedExpr::Known(y)) => Ok(PointToAlign::Fixed { x, y }),
4252                (UnsolvedExpr::Unknown(x), UnsolvedExpr::Unknown(y)) => Ok(PointToAlign::Variable { x, y }),
4253                (UnsolvedExpr::Known(..), UnsolvedExpr::Unknown(..)) => {
4254                    Err(KclError::new_semantic(KclErrorDetails::new(
4255                        format!(
4256                            "The `{}` function cannot take a fixed X component and a variable Y component",
4257                            kind.function_name()
4258                        ),
4259                        vec![source_range],
4260                    )))
4261                }
4262                (UnsolvedExpr::Unknown(..), UnsolvedExpr::Known(..)) => {
4263                    Err(KclError::new_semantic(KclErrorDetails::new(
4264                        format!(
4265                            "The `{}` function cannot take a fixed X component and a variable Y component",
4266                            kind.function_name()
4267                        ),
4268                        vec![source_range],
4269                    )))
4270                }
4271            }
4272        }
4273        _ => Err(KclError::new_semantic(KclErrorDetails::new(
4274            format!(
4275                "The `{}` function accepts either a line Segment or a list of points",
4276                kind.function_name()
4277            ),
4278            vec![source_range],
4279        ))),
4280    }
4281}
4282
4283async fn axis_constraint(
4284    kind: AxisConstraintKind,
4285    exec_state: &mut ExecState,
4286    args: Args,
4287) -> Result<KclValue, KclError> {
4288    let input: KclValue =
4289        args.get_unlabeled_kw_arg("input", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
4290
4291    // User could pass in a single line, or a sequence of points.
4292    match input {
4293        KclValue::Segment { value } => {
4294            // Single-line case.
4295            axis_constraint_line(value, kind, exec_state, args)
4296        }
4297        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
4298            // Sequence of points case.
4299            axis_constraint_points(value, kind, exec_state, args)
4300        }
4301        other => Err(KclError::new_semantic(KclErrorDetails::new(
4302            format!(
4303                "{}() accepts either a line Segment or a list of at least two points, but you provided {}",
4304                kind.function_name(),
4305                other.human_friendly_type(),
4306            ),
4307            vec![args.source_range],
4308        ))),
4309    }
4310}
4311
4312/// User has provided a single line to align along the given axis.
4313fn axis_constraint_line(
4314    segment: Box<AbstractSegment>,
4315    kind: AxisConstraintKind,
4316    exec_state: &mut ExecState,
4317    args: Args,
4318) -> Result<KclValue, KclError> {
4319    let line = extract_axis_line_vars(&segment, kind, args.source_range)?;
4320    let range = args.source_range;
4321    let solver_p0 = DatumPoint::new_xy(
4322        line.start[0].to_constraint_id(range)?,
4323        line.start[1].to_constraint_id(range)?,
4324    );
4325    let solver_p1 = DatumPoint::new_xy(
4326        line.end[0].to_constraint_id(range)?,
4327        line.end[1].to_constraint_id(range)?,
4328    );
4329    let solver_line = DatumLineSegment::new(solver_p0, solver_p1);
4330    let constraint = kind.line_constraint(solver_line);
4331    #[cfg(feature = "artifact-graph")]
4332    let constraint_id = exec_state.next_object_id();
4333    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4334        return Err(KclError::new_semantic(KclErrorDetails::new(
4335            format!("{}() can only be used inside a sketch block", kind.function_name()),
4336            vec![args.source_range],
4337        )));
4338    };
4339    sketch_state.solver_constraints.push(constraint);
4340    #[cfg(feature = "artifact-graph")]
4341    {
4342        let constraint = kind.line_artifact_constraint(line.object_id);
4343        sketch_state.sketch_constraints.push(constraint_id);
4344        track_constraint(constraint_id, constraint, exec_state, &args);
4345    }
4346    Ok(KclValue::none())
4347}
4348
4349/// User has provided a sequence of points to align along the given axis.
4350fn axis_constraint_points(
4351    point_values: Vec<KclValue>,
4352    kind: AxisConstraintKind,
4353    exec_state: &mut ExecState,
4354    args: Args,
4355) -> Result<KclValue, KclError> {
4356    if point_values.len() < 2 {
4357        return Err(KclError::new_semantic(KclErrorDetails::new(
4358            format!("{}() point list must contain at least two points", kind.function_name()),
4359            vec![args.source_range],
4360        )));
4361    }
4362
4363    #[cfg(feature = "artifact-graph")]
4364    let trackable_point_ids = point_values
4365        .iter()
4366        .map(|point| match point {
4367            KclValue::Segment { value: segment } => {
4368                let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
4369                    return None;
4370                };
4371                let UnsolvedSegmentKind::Point { .. } = &unsolved.kind else {
4372                    return None;
4373                };
4374                Some(ConstraintSegment::from(unsolved.object_id))
4375            }
4376            point if point2d_is_origin(point) => Some(ConstraintSegment::ORIGIN),
4377            _ => None,
4378        })
4379        .collect::<Option<Vec<_>>>();
4380
4381    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4382        return Err(KclError::new_semantic(KclErrorDetails::new(
4383            format!("{}() can only be used inside a sketch block", kind.function_name()),
4384            vec![args.source_range],
4385        )));
4386    };
4387
4388    let points: Vec<PointToAlign> = point_values
4389        .iter()
4390        .map(|point| extract_axis_point_vars(point, kind, args.source_range))
4391        .collect::<Result<_, _>>()?;
4392
4393    let mut solver_constraints = Vec::with_capacity(points.len().saturating_sub(1));
4394
4395    let mut var_points = Vec::new();
4396    let mut fix_points = Vec::new();
4397    for point in points {
4398        match point {
4399            PointToAlign::Variable { x, y } => var_points.push((x, y)),
4400            PointToAlign::Fixed { x, y } => fix_points.push((x, y)),
4401        }
4402    }
4403    if fix_points.len() > 1 {
4404        return Err(KclError::new_semantic(KclErrorDetails::new(
4405            format!(
4406                "{}() point list can contain at most 1 fixed point, but you provided {}",
4407                kind.function_name(),
4408                fix_points.len()
4409            ),
4410            vec![args.source_range],
4411        )));
4412    }
4413
4414    if let Some(fix_point) = fix_points.pop() {
4415        // We have to align all the variable points with this singular fixed point.
4416        // For points 0, 1, 2, ..., n, create constraints
4417        // fixed(0.x, fix.x)
4418        // fixed(1.x, fix.x)
4419        // ...
4420        // fixed(n.x, fix.x)
4421        // (or y, whatever is appropriate)
4422        for point in var_points {
4423            let solver_point = datum_point([point.0, point.1], args.source_range)?;
4424            let fix_point_mm = (fix_point.0.to_mm(), fix_point.1.to_mm());
4425            solver_constraints.push(kind.constraint_aligning_point_to_constant(solver_point, fix_point_mm));
4426        }
4427    } else {
4428        // For points 0, 1, 2, ..., n, create constraints
4429        // vertical(0, 1)
4430        // vertical(0, 2)
4431        // ...
4432        // vertical(0, n)
4433        // (or horizontal, if appropriate)
4434        let mut points = var_points.into_iter();
4435        let first_point = points.next().ok_or_else(|| {
4436            KclError::new_semantic(KclErrorDetails::new(
4437                format!("{}() point list must contain at least two points", kind.function_name()),
4438                vec![args.source_range],
4439            ))
4440        })?;
4441        let anchor = datum_point([first_point.0, first_point.1], args.source_range)?;
4442        for point in points {
4443            let solver_point = datum_point([point.0, point.1], args.source_range)?;
4444            solver_constraints.push(kind.point_pair_constraint(anchor, solver_point));
4445        }
4446    }
4447    sketch_state.solver_constraints.extend(solver_constraints);
4448
4449    #[cfg(feature = "artifact-graph")]
4450    if let Some(point_ids) = trackable_point_ids {
4451        let constraint_id = exec_state.next_object_id();
4452        let Some(sketch_state) = exec_state.sketch_block_mut() else {
4453            debug_assert!(false, "Constraint created outside a sketch block");
4454            return Ok(KclValue::none());
4455        };
4456        sketch_state.sketch_constraints.push(constraint_id);
4457        let constraint = kind.point_artifact_constraint(point_ids);
4458        track_constraint(constraint_id, constraint, exec_state, &args);
4459    }
4460
4461    Ok(KclValue::none())
4462}
4463
4464pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4465    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
4466        "lines",
4467        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
4468        exec_state,
4469    )?;
4470    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
4471        KclError::new_semantic(KclErrorDetails::new(
4472            "must have two input lines".to_owned(),
4473            vec![args.source_range],
4474        ))
4475    })?;
4476    let KclValue::Segment { value: segment0 } = &line0 else {
4477        return Err(KclError::new_semantic(KclErrorDetails::new(
4478            "line argument must be a Segment".to_owned(),
4479            vec![args.source_range],
4480        )));
4481    };
4482    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
4483        return Err(KclError::new_internal(KclErrorDetails::new(
4484            "line must be an unsolved Segment".to_owned(),
4485            vec![args.source_range],
4486        )));
4487    };
4488    let UnsolvedSegmentKind::Line {
4489        start: start0,
4490        end: end0,
4491        ..
4492    } = &unsolved0.kind
4493    else {
4494        return Err(KclError::new_semantic(KclErrorDetails::new(
4495            "line argument must be a line, no other type of Segment".to_owned(),
4496            vec![args.source_range],
4497        )));
4498    };
4499    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
4500        return Err(KclError::new_semantic(KclErrorDetails::new(
4501            "line's start x coordinate must be a var".to_owned(),
4502            vec![args.source_range],
4503        )));
4504    };
4505    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
4506        return Err(KclError::new_semantic(KclErrorDetails::new(
4507            "line's start y coordinate must be a var".to_owned(),
4508            vec![args.source_range],
4509        )));
4510    };
4511    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
4512        return Err(KclError::new_semantic(KclErrorDetails::new(
4513            "line's end x coordinate must be a var".to_owned(),
4514            vec![args.source_range],
4515        )));
4516    };
4517    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
4518        return Err(KclError::new_semantic(KclErrorDetails::new(
4519            "line's end y coordinate must be a var".to_owned(),
4520            vec![args.source_range],
4521        )));
4522    };
4523    let KclValue::Segment { value: segment1 } = &line1 else {
4524        return Err(KclError::new_semantic(KclErrorDetails::new(
4525            "line argument must be a Segment".to_owned(),
4526            vec![args.source_range],
4527        )));
4528    };
4529    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
4530        return Err(KclError::new_internal(KclErrorDetails::new(
4531            "line must be an unsolved Segment".to_owned(),
4532            vec![args.source_range],
4533        )));
4534    };
4535    let UnsolvedSegmentKind::Line {
4536        start: start1,
4537        end: end1,
4538        ..
4539    } = &unsolved1.kind
4540    else {
4541        return Err(KclError::new_semantic(KclErrorDetails::new(
4542            "line argument must be a line, no other type of Segment".to_owned(),
4543            vec![args.source_range],
4544        )));
4545    };
4546    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
4547        return Err(KclError::new_semantic(KclErrorDetails::new(
4548            "line's start x coordinate must be a var".to_owned(),
4549            vec![args.source_range],
4550        )));
4551    };
4552    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
4553        return Err(KclError::new_semantic(KclErrorDetails::new(
4554            "line's start y coordinate must be a var".to_owned(),
4555            vec![args.source_range],
4556        )));
4557    };
4558    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
4559        return Err(KclError::new_semantic(KclErrorDetails::new(
4560            "line's end x coordinate must be a var".to_owned(),
4561            vec![args.source_range],
4562        )));
4563    };
4564    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
4565        return Err(KclError::new_semantic(KclErrorDetails::new(
4566            "line's end y coordinate must be a var".to_owned(),
4567            vec![args.source_range],
4568        )));
4569    };
4570
4571    // All coordinates are sketch vars. Proceed.
4572    let sketch_constraint = SketchConstraint {
4573        kind: SketchConstraintKind::Angle {
4574            line0: crate::execution::ConstrainableLine2d {
4575                object_id: unsolved0.object_id,
4576                vars: [
4577                    crate::front::Point2d {
4578                        x: *line0_p0_x,
4579                        y: *line0_p0_y,
4580                    },
4581                    crate::front::Point2d {
4582                        x: *line0_p1_x,
4583                        y: *line0_p1_y,
4584                    },
4585                ],
4586            },
4587            line1: crate::execution::ConstrainableLine2d {
4588                object_id: unsolved1.object_id,
4589                vars: [
4590                    crate::front::Point2d {
4591                        x: *line1_p0_x,
4592                        y: *line1_p0_y,
4593                    },
4594                    crate::front::Point2d {
4595                        x: *line1_p1_x,
4596                        y: *line1_p1_y,
4597                    },
4598                ],
4599            },
4600        },
4601        meta: vec![args.source_range.into()],
4602    };
4603    Ok(KclValue::SketchConstraint {
4604        value: Box::new(sketch_constraint),
4605    })
4606}
4607
4608async fn lines_at_angle(
4609    angle_kind: LinesAtAngleKind,
4610    exec_state: &mut ExecState,
4611    args: Args,
4612) -> Result<KclValue, KclError> {
4613    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
4614        "lines",
4615        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
4616        exec_state,
4617    )?;
4618    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
4619        KclError::new_semantic(KclErrorDetails::new(
4620            "must have two input lines".to_owned(),
4621            vec![args.source_range],
4622        ))
4623    })?;
4624
4625    let KclValue::Segment { value: segment0 } = &line0 else {
4626        return Err(KclError::new_semantic(KclErrorDetails::new(
4627            "line argument must be a Segment".to_owned(),
4628            vec![args.source_range],
4629        )));
4630    };
4631    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
4632        return Err(KclError::new_internal(KclErrorDetails::new(
4633            "line must be an unsolved Segment".to_owned(),
4634            vec![args.source_range],
4635        )));
4636    };
4637    let UnsolvedSegmentKind::Line {
4638        start: start0,
4639        end: end0,
4640        ..
4641    } = &unsolved0.kind
4642    else {
4643        return Err(KclError::new_semantic(KclErrorDetails::new(
4644            "line argument must be a line, no other type of Segment".to_owned(),
4645            vec![args.source_range],
4646        )));
4647    };
4648    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
4649        return Err(KclError::new_semantic(KclErrorDetails::new(
4650            "line's start x coordinate must be a var".to_owned(),
4651            vec![args.source_range],
4652        )));
4653    };
4654    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
4655        return Err(KclError::new_semantic(KclErrorDetails::new(
4656            "line's start y coordinate must be a var".to_owned(),
4657            vec![args.source_range],
4658        )));
4659    };
4660    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
4661        return Err(KclError::new_semantic(KclErrorDetails::new(
4662            "line's end x coordinate must be a var".to_owned(),
4663            vec![args.source_range],
4664        )));
4665    };
4666    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
4667        return Err(KclError::new_semantic(KclErrorDetails::new(
4668            "line's end y coordinate must be a var".to_owned(),
4669            vec![args.source_range],
4670        )));
4671    };
4672    let KclValue::Segment { value: segment1 } = &line1 else {
4673        return Err(KclError::new_semantic(KclErrorDetails::new(
4674            "line argument must be a Segment".to_owned(),
4675            vec![args.source_range],
4676        )));
4677    };
4678    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
4679        return Err(KclError::new_internal(KclErrorDetails::new(
4680            "line must be an unsolved Segment".to_owned(),
4681            vec![args.source_range],
4682        )));
4683    };
4684    let UnsolvedSegmentKind::Line {
4685        start: start1,
4686        end: end1,
4687        ..
4688    } = &unsolved1.kind
4689    else {
4690        return Err(KclError::new_semantic(KclErrorDetails::new(
4691            "line argument must be a line, no other type of Segment".to_owned(),
4692            vec![args.source_range],
4693        )));
4694    };
4695    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
4696        return Err(KclError::new_semantic(KclErrorDetails::new(
4697            "line's start x coordinate must be a var".to_owned(),
4698            vec![args.source_range],
4699        )));
4700    };
4701    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
4702        return Err(KclError::new_semantic(KclErrorDetails::new(
4703            "line's start y coordinate must be a var".to_owned(),
4704            vec![args.source_range],
4705        )));
4706    };
4707    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
4708        return Err(KclError::new_semantic(KclErrorDetails::new(
4709            "line's end x coordinate must be a var".to_owned(),
4710            vec![args.source_range],
4711        )));
4712    };
4713    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
4714        return Err(KclError::new_semantic(KclErrorDetails::new(
4715            "line's end y coordinate must be a var".to_owned(),
4716            vec![args.source_range],
4717        )));
4718    };
4719
4720    let range = args.source_range;
4721    let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4722        line0_p0_x.to_constraint_id(range)?,
4723        line0_p0_y.to_constraint_id(range)?,
4724    );
4725    let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4726        line0_p1_x.to_constraint_id(range)?,
4727        line0_p1_y.to_constraint_id(range)?,
4728    );
4729    let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
4730    let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4731        line1_p0_x.to_constraint_id(range)?,
4732        line1_p0_y.to_constraint_id(range)?,
4733    );
4734    let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4735        line1_p1_x.to_constraint_id(range)?,
4736        line1_p1_y.to_constraint_id(range)?,
4737    );
4738    let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
4739    let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
4740    #[cfg(feature = "artifact-graph")]
4741    let constraint_id = exec_state.next_object_id();
4742    // Save the constraint to be used for solving.
4743    let Some(sketch_state) = exec_state.sketch_block_mut() else {
4744        return Err(KclError::new_semantic(KclErrorDetails::new(
4745            format!(
4746                "{}() can only be used inside a sketch block",
4747                angle_kind.to_function_name()
4748            ),
4749            vec![args.source_range],
4750        )));
4751    };
4752    sketch_state.solver_constraints.push(constraint);
4753    #[cfg(feature = "artifact-graph")]
4754    {
4755        let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
4756        sketch_state.sketch_constraints.push(constraint_id);
4757        track_constraint(constraint_id, constraint, exec_state, &args);
4758    }
4759    Ok(KclValue::none())
4760}
4761
4762pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4763    axis_constraint(AxisConstraintKind::Horizontal, exec_state, args).await
4764}
4765
4766pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
4767    axis_constraint(AxisConstraintKind::Vertical, exec_state, args).await
4768}