Skip to main content

kcl_lib/std/
constraints.rs

1use anyhow::Result;
2use ezpz::Constraint as SolverConstraint;
3use ezpz::datatypes::AngleKind;
4use ezpz::datatypes::inputs::DatumCircle;
5use ezpz::datatypes::inputs::DatumCircularArc;
6use ezpz::datatypes::inputs::DatumDistance;
7use ezpz::datatypes::inputs::DatumLineSegment;
8use ezpz::datatypes::inputs::DatumPoint;
9use kittycad_modeling_cmds as kcmc;
10
11use crate::errors::KclError;
12use crate::errors::KclErrorDetails;
13use crate::execution::AbstractSegment;
14#[cfg(feature = "artifact-graph")]
15use crate::execution::Artifact;
16#[cfg(feature = "artifact-graph")]
17use crate::execution::CodeRef;
18use crate::execution::ConstrainablePoint2d;
19use crate::execution::ConstrainablePoint2dOrOrigin;
20use crate::execution::ExecState;
21use crate::execution::KclValue;
22use crate::execution::SegmentRepr;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::SketchBlockConstraint;
25#[cfg(feature = "artifact-graph")]
26use crate::execution::SketchBlockConstraintType;
27use crate::execution::SketchConstraint;
28use crate::execution::SketchConstraintKind;
29use crate::execution::SketchVarId;
30use crate::execution::UnsolvedExpr;
31use crate::execution::UnsolvedSegment;
32use crate::execution::UnsolvedSegmentKind;
33use crate::execution::normalize_to_solver_distance_unit;
34use crate::execution::solver_numeric_type;
35use crate::execution::types::ArrayLen;
36use crate::execution::types::PrimitiveType;
37use crate::execution::types::RuntimeType;
38use crate::front::ArcCtor;
39use crate::front::CircleCtor;
40#[cfg(feature = "artifact-graph")]
41use crate::front::Coincident;
42#[cfg(feature = "artifact-graph")]
43use crate::front::Constraint;
44#[cfg(feature = "artifact-graph")]
45use crate::front::EqualRadius;
46#[cfg(feature = "artifact-graph")]
47use crate::front::Horizontal;
48use crate::front::LineCtor;
49#[cfg(feature = "artifact-graph")]
50use crate::front::LinesEqualLength;
51#[cfg(feature = "artifact-graph")]
52use crate::front::Object;
53use crate::front::ObjectId;
54#[cfg(feature = "artifact-graph")]
55use crate::front::ObjectKind;
56#[cfg(feature = "artifact-graph")]
57use crate::front::Parallel;
58#[cfg(feature = "artifact-graph")]
59use crate::front::Perpendicular;
60use crate::front::Point2d;
61use crate::front::PointCtor;
62#[cfg(feature = "artifact-graph")]
63use crate::front::SourceRef;
64#[cfg(feature = "artifact-graph")]
65use crate::front::Tangent;
66#[cfg(feature = "artifact-graph")]
67use crate::front::Vertical;
68#[cfg(feature = "artifact-graph")]
69use crate::frontend::sketch::ConstraintSegment;
70use crate::std::Args;
71use crate::std::args::FromKclValue;
72use crate::std::args::TyF64;
73
74fn point2d_is_origin(point2d: &KclValue) -> bool {
75    let Some([x, y]) = <[TyF64; 2]>::from_kcl_val(point2d) else {
76        return false;
77    };
78    // Both components must be lengths (not angles or unknown types).
79    // as_length() returns None for non-length types.
80    if x.ty.as_length().is_none() || y.ty.as_length().is_none() {
81        return false;
82    }
83    // Now that we've checked that they're lengths, the exact units don't
84    // matter. We only care that the value is zero.
85    x.n == 0.0 && y.n == 0.0
86}
87
88#[cfg(feature = "artifact-graph")]
89fn coincident_segments_for_segment_and_point2d(
90    segment_id: ObjectId,
91    point2d: &KclValue,
92    segment_first: bool,
93) -> Vec<ConstraintSegment> {
94    if !point2d_is_origin(point2d) {
95        return vec![segment_id.into()];
96    }
97
98    if segment_first {
99        vec![segment_id.into(), ConstraintSegment::ORIGIN]
100    } else {
101        vec![ConstraintSegment::ORIGIN, segment_id.into()]
102    }
103}
104
105pub async fn point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
106    let at: Vec<KclValue> = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
107    let [at_x_value, at_y_value]: [KclValue; 2] = at.try_into().map_err(|_| {
108        KclError::new_semantic(KclErrorDetails::new(
109            "at must be a 2D point".to_owned(),
110            vec![args.source_range],
111        ))
112    })?;
113    let Some(at_x) = at_x_value.as_unsolved_expr() else {
114        return Err(KclError::new_semantic(KclErrorDetails::new(
115            "at x must be a number or sketch var".to_owned(),
116            vec![args.source_range],
117        )));
118    };
119    let Some(at_y) = at_y_value.as_unsolved_expr() else {
120        return Err(KclError::new_semantic(KclErrorDetails::new(
121            "at y must be a number or sketch var".to_owned(),
122            vec![args.source_range],
123        )));
124    };
125    let ctor = PointCtor {
126        position: Point2d {
127            x: at_x_value.to_sketch_expr().ok_or_else(|| {
128                KclError::new_semantic(KclErrorDetails::new(
129                    "unable to convert numeric type to suffix".to_owned(),
130                    vec![args.source_range],
131                ))
132            })?,
133            y: at_y_value.to_sketch_expr().ok_or_else(|| {
134                KclError::new_semantic(KclErrorDetails::new(
135                    "unable to convert numeric type to suffix".to_owned(),
136                    vec![args.source_range],
137                ))
138            })?,
139        },
140    };
141    let segment = UnsolvedSegment {
142        id: exec_state.next_uuid(),
143        object_id: exec_state.next_object_id(),
144        kind: UnsolvedSegmentKind::Point {
145            position: [at_x, at_y],
146            ctor: Box::new(ctor),
147        },
148        tag: None,
149        node_path: args.node_path.clone(),
150        meta: vec![args.source_range.into()],
151    };
152    #[cfg(feature = "artifact-graph")]
153    let optional_constraints = {
154        let object_id = exec_state.add_placeholder_scene_object(segment.object_id, args.source_range, args.node_path);
155
156        let mut optional_constraints = Vec::new();
157        if exec_state.segment_ids_edited_contains(&object_id) {
158            if let Some(at_x_var) = at_x_value.as_sketch_var() {
159                let x_initial_value = at_x_var.initial_value_to_solver_units(
160                    exec_state,
161                    args.source_range,
162                    "edited segment fixed constraint value",
163                )?;
164                optional_constraints.push(SolverConstraint::Fixed(
165                    at_x_var.id.to_constraint_id(args.source_range)?,
166                    x_initial_value.n,
167                ));
168            }
169            if let Some(at_y_var) = at_y_value.as_sketch_var() {
170                let y_initial_value = at_y_var.initial_value_to_solver_units(
171                    exec_state,
172                    args.source_range,
173                    "edited segment fixed constraint value",
174                )?;
175                optional_constraints.push(SolverConstraint::Fixed(
176                    at_y_var.id.to_constraint_id(args.source_range)?,
177                    y_initial_value.n,
178                ));
179            }
180        }
181        optional_constraints
182    };
183
184    // Save the segment to be sent to the engine after solving.
185    let Some(sketch_state) = exec_state.sketch_block_mut() else {
186        return Err(KclError::new_semantic(KclErrorDetails::new(
187            "line() can only be used inside a sketch block".to_owned(),
188            vec![args.source_range],
189        )));
190    };
191    sketch_state.needed_by_engine.push(segment.clone());
192
193    #[cfg(feature = "artifact-graph")]
194    sketch_state.solver_optional_constraints.extend(optional_constraints);
195
196    let meta = segment.meta.clone();
197    let abstract_segment = AbstractSegment {
198        repr: SegmentRepr::Unsolved {
199            segment: Box::new(segment),
200        },
201        meta,
202    };
203    Ok(KclValue::Segment {
204        value: Box::new(abstract_segment),
205    })
206}
207
208pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
209    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
210    let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
211    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
212    let construction: bool = construction_opt.unwrap_or(false);
213    let construction_ctor = construction_opt;
214    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
215        KclError::new_semantic(KclErrorDetails::new(
216            "start must be a 2D point".to_owned(),
217            vec![args.source_range],
218        ))
219    })?;
220    let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
221        KclError::new_semantic(KclErrorDetails::new(
222            "end must be a 2D point".to_owned(),
223            vec![args.source_range],
224        ))
225    })?;
226    let Some(start_x) = start_x_value.as_unsolved_expr() else {
227        return Err(KclError::new_semantic(KclErrorDetails::new(
228            "start x must be a number or sketch var".to_owned(),
229            vec![args.source_range],
230        )));
231    };
232    let Some(start_y) = start_y_value.as_unsolved_expr() else {
233        return Err(KclError::new_semantic(KclErrorDetails::new(
234            "start y must be a number or sketch var".to_owned(),
235            vec![args.source_range],
236        )));
237    };
238    let Some(end_x) = end_x_value.as_unsolved_expr() else {
239        return Err(KclError::new_semantic(KclErrorDetails::new(
240            "end x must be a number or sketch var".to_owned(),
241            vec![args.source_range],
242        )));
243    };
244    let Some(end_y) = end_y_value.as_unsolved_expr() else {
245        return Err(KclError::new_semantic(KclErrorDetails::new(
246            "end y must be a number or sketch var".to_owned(),
247            vec![args.source_range],
248        )));
249    };
250    let ctor = LineCtor {
251        start: Point2d {
252            x: start_x_value.to_sketch_expr().ok_or_else(|| {
253                KclError::new_semantic(KclErrorDetails::new(
254                    "unable to convert numeric type to suffix".to_owned(),
255                    vec![args.source_range],
256                ))
257            })?,
258            y: start_y_value.to_sketch_expr().ok_or_else(|| {
259                KclError::new_semantic(KclErrorDetails::new(
260                    "unable to convert numeric type to suffix".to_owned(),
261                    vec![args.source_range],
262                ))
263            })?,
264        },
265        end: Point2d {
266            x: end_x_value.to_sketch_expr().ok_or_else(|| {
267                KclError::new_semantic(KclErrorDetails::new(
268                    "unable to convert numeric type to suffix".to_owned(),
269                    vec![args.source_range],
270                ))
271            })?,
272            y: end_y_value.to_sketch_expr().ok_or_else(|| {
273                KclError::new_semantic(KclErrorDetails::new(
274                    "unable to convert numeric type to suffix".to_owned(),
275                    vec![args.source_range],
276                ))
277            })?,
278        },
279        construction: construction_ctor,
280    };
281    // Order of ID generation is important.
282    let start_object_id = exec_state.next_object_id();
283    let end_object_id = exec_state.next_object_id();
284    let line_object_id = exec_state.next_object_id();
285    let segment = UnsolvedSegment {
286        id: exec_state.next_uuid(),
287        object_id: line_object_id,
288        kind: UnsolvedSegmentKind::Line {
289            start: [start_x, start_y],
290            end: [end_x, end_y],
291            ctor: Box::new(ctor),
292            start_object_id,
293            end_object_id,
294            construction,
295        },
296        tag: None,
297        node_path: args.node_path.clone(),
298        meta: vec![args.source_range.into()],
299    };
300    #[cfg(feature = "artifact-graph")]
301    let optional_constraints = {
302        let start_object_id =
303            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
304        let end_object_id =
305            exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
306        let line_object_id =
307            exec_state.add_placeholder_scene_object(line_object_id, args.source_range, args.node_path.clone());
308
309        let mut optional_constraints = Vec::new();
310        if exec_state.segment_ids_edited_contains(&start_object_id)
311            || exec_state.segment_ids_edited_contains(&line_object_id)
312        {
313            if let Some(start_x_var) = start_x_value.as_sketch_var() {
314                let x_initial_value = start_x_var.initial_value_to_solver_units(
315                    exec_state,
316                    args.source_range,
317                    "edited segment fixed constraint value",
318                )?;
319                optional_constraints.push(SolverConstraint::Fixed(
320                    start_x_var.id.to_constraint_id(args.source_range)?,
321                    x_initial_value.n,
322                ));
323            }
324            if let Some(start_y_var) = start_y_value.as_sketch_var() {
325                let y_initial_value = start_y_var.initial_value_to_solver_units(
326                    exec_state,
327                    args.source_range,
328                    "edited segment fixed constraint value",
329                )?;
330                optional_constraints.push(SolverConstraint::Fixed(
331                    start_y_var.id.to_constraint_id(args.source_range)?,
332                    y_initial_value.n,
333                ));
334            }
335        }
336        if exec_state.segment_ids_edited_contains(&end_object_id)
337            || exec_state.segment_ids_edited_contains(&line_object_id)
338        {
339            if let Some(end_x_var) = end_x_value.as_sketch_var() {
340                let x_initial_value = end_x_var.initial_value_to_solver_units(
341                    exec_state,
342                    args.source_range,
343                    "edited segment fixed constraint value",
344                )?;
345                optional_constraints.push(SolverConstraint::Fixed(
346                    end_x_var.id.to_constraint_id(args.source_range)?,
347                    x_initial_value.n,
348                ));
349            }
350            if let Some(end_y_var) = end_y_value.as_sketch_var() {
351                let y_initial_value = end_y_var.initial_value_to_solver_units(
352                    exec_state,
353                    args.source_range,
354                    "edited segment fixed constraint value",
355                )?;
356                optional_constraints.push(SolverConstraint::Fixed(
357                    end_y_var.id.to_constraint_id(args.source_range)?,
358                    y_initial_value.n,
359                ));
360            }
361        }
362        optional_constraints
363    };
364
365    // Save the segment to be sent to the engine after solving.
366    let Some(sketch_state) = exec_state.sketch_block_mut() else {
367        return Err(KclError::new_semantic(KclErrorDetails::new(
368            "line() can only be used inside a sketch block".to_owned(),
369            vec![args.source_range],
370        )));
371    };
372    sketch_state.needed_by_engine.push(segment.clone());
373
374    #[cfg(feature = "artifact-graph")]
375    sketch_state.solver_optional_constraints.extend(optional_constraints);
376
377    let meta = segment.meta.clone();
378    let abstract_segment = AbstractSegment {
379        repr: SegmentRepr::Unsolved {
380            segment: Box::new(segment),
381        },
382        meta,
383    };
384    Ok(KclValue::Segment {
385        value: Box::new(abstract_segment),
386    })
387}
388
389pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
390    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
391    let end: Vec<KclValue> = args.get_kw_arg("end", &RuntimeType::point2d(), exec_state)?;
392    // TODO: make this optional and add interior.
393    let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
394    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
395    let construction: bool = construction_opt.unwrap_or(false);
396    let construction_ctor = construction_opt;
397
398    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
399        KclError::new_semantic(KclErrorDetails::new(
400            "start must be a 2D point".to_owned(),
401            vec![args.source_range],
402        ))
403    })?;
404    let [end_x_value, end_y_value]: [KclValue; 2] = end.try_into().map_err(|_| {
405        KclError::new_semantic(KclErrorDetails::new(
406            "end must be a 2D point".to_owned(),
407            vec![args.source_range],
408        ))
409    })?;
410    let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
411        KclError::new_semantic(KclErrorDetails::new(
412            "center must be a 2D point".to_owned(),
413            vec![args.source_range],
414        ))
415    })?;
416
417    let Some(UnsolvedExpr::Unknown(start_x)) = start_x_value.as_unsolved_expr() else {
418        return Err(KclError::new_semantic(KclErrorDetails::new(
419            "start x must be a sketch var".to_owned(),
420            vec![args.source_range],
421        )));
422    };
423    let Some(UnsolvedExpr::Unknown(start_y)) = start_y_value.as_unsolved_expr() else {
424        return Err(KclError::new_semantic(KclErrorDetails::new(
425            "start y must be a sketch var".to_owned(),
426            vec![args.source_range],
427        )));
428    };
429    let Some(UnsolvedExpr::Unknown(end_x)) = end_x_value.as_unsolved_expr() else {
430        return Err(KclError::new_semantic(KclErrorDetails::new(
431            "end x must be a sketch var".to_owned(),
432            vec![args.source_range],
433        )));
434    };
435    let Some(UnsolvedExpr::Unknown(end_y)) = end_y_value.as_unsolved_expr() else {
436        return Err(KclError::new_semantic(KclErrorDetails::new(
437            "end y must be a sketch var".to_owned(),
438            vec![args.source_range],
439        )));
440    };
441    let Some(UnsolvedExpr::Unknown(center_x)) = center_x_value.as_unsolved_expr() else {
442        return Err(KclError::new_semantic(KclErrorDetails::new(
443            "center x must be a sketch var".to_owned(),
444            vec![args.source_range],
445        )));
446    };
447    let Some(UnsolvedExpr::Unknown(center_y)) = center_y_value.as_unsolved_expr() else {
448        return Err(KclError::new_semantic(KclErrorDetails::new(
449            "center y must be a sketch var".to_owned(),
450            vec![args.source_range],
451        )));
452    };
453
454    let ctor = ArcCtor {
455        start: Point2d {
456            x: start_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: start_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        end: Point2d {
470            x: end_x_value.to_sketch_expr().ok_or_else(|| {
471                KclError::new_semantic(KclErrorDetails::new(
472                    "unable to convert numeric type to suffix".to_owned(),
473                    vec![args.source_range],
474                ))
475            })?,
476            y: end_y_value.to_sketch_expr().ok_or_else(|| {
477                KclError::new_semantic(KclErrorDetails::new(
478                    "unable to convert numeric type to suffix".to_owned(),
479                    vec![args.source_range],
480                ))
481            })?,
482        },
483        center: Point2d {
484            x: center_x_value.to_sketch_expr().ok_or_else(|| {
485                KclError::new_semantic(KclErrorDetails::new(
486                    "unable to convert numeric type to suffix".to_owned(),
487                    vec![args.source_range],
488                ))
489            })?,
490            y: center_y_value.to_sketch_expr().ok_or_else(|| {
491                KclError::new_semantic(KclErrorDetails::new(
492                    "unable to convert numeric type to suffix".to_owned(),
493                    vec![args.source_range],
494                ))
495            })?,
496        },
497        construction: construction_ctor,
498    };
499
500    // Order of ID generation is important.
501    let start_object_id = exec_state.next_object_id();
502    let end_object_id = exec_state.next_object_id();
503    let center_object_id = exec_state.next_object_id();
504    let arc_object_id = exec_state.next_object_id();
505    let segment = UnsolvedSegment {
506        id: exec_state.next_uuid(),
507        object_id: arc_object_id,
508        kind: UnsolvedSegmentKind::Arc {
509            start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
510            end: [UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)],
511            center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
512            ctor: Box::new(ctor),
513            start_object_id,
514            end_object_id,
515            center_object_id,
516            construction,
517        },
518        tag: None,
519        node_path: args.node_path.clone(),
520        meta: vec![args.source_range.into()],
521    };
522    #[cfg(feature = "artifact-graph")]
523    let optional_constraints = {
524        let start_object_id =
525            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
526        let end_object_id =
527            exec_state.add_placeholder_scene_object(end_object_id, args.source_range, args.node_path.clone());
528        let center_object_id =
529            exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
530        let arc_object_id =
531            exec_state.add_placeholder_scene_object(arc_object_id, args.source_range, args.node_path.clone());
532
533        let mut optional_constraints = Vec::new();
534        if exec_state.segment_ids_edited_contains(&start_object_id)
535            || exec_state.segment_ids_edited_contains(&arc_object_id)
536        {
537            if let Some(start_x_var) = start_x_value.as_sketch_var() {
538                let x_initial_value = start_x_var.initial_value_to_solver_units(
539                    exec_state,
540                    args.source_range,
541                    "edited segment fixed constraint value",
542                )?;
543                optional_constraints.push(ezpz::Constraint::Fixed(
544                    start_x_var.id.to_constraint_id(args.source_range)?,
545                    x_initial_value.n,
546                ));
547            }
548            if let Some(start_y_var) = start_y_value.as_sketch_var() {
549                let y_initial_value = start_y_var.initial_value_to_solver_units(
550                    exec_state,
551                    args.source_range,
552                    "edited segment fixed constraint value",
553                )?;
554                optional_constraints.push(ezpz::Constraint::Fixed(
555                    start_y_var.id.to_constraint_id(args.source_range)?,
556                    y_initial_value.n,
557                ));
558            }
559        }
560        if exec_state.segment_ids_edited_contains(&end_object_id)
561            || exec_state.segment_ids_edited_contains(&arc_object_id)
562        {
563            if let Some(end_x_var) = end_x_value.as_sketch_var() {
564                let x_initial_value = end_x_var.initial_value_to_solver_units(
565                    exec_state,
566                    args.source_range,
567                    "edited segment fixed constraint value",
568                )?;
569                optional_constraints.push(ezpz::Constraint::Fixed(
570                    end_x_var.id.to_constraint_id(args.source_range)?,
571                    x_initial_value.n,
572                ));
573            }
574            if let Some(end_y_var) = end_y_value.as_sketch_var() {
575                let y_initial_value = end_y_var.initial_value_to_solver_units(
576                    exec_state,
577                    args.source_range,
578                    "edited segment fixed constraint value",
579                )?;
580                optional_constraints.push(ezpz::Constraint::Fixed(
581                    end_y_var.id.to_constraint_id(args.source_range)?,
582                    y_initial_value.n,
583                ));
584            }
585        }
586        if exec_state.segment_ids_edited_contains(&center_object_id)
587            || exec_state.segment_ids_edited_contains(&arc_object_id)
588        {
589            if let Some(center_x_var) = center_x_value.as_sketch_var() {
590                let x_initial_value = center_x_var.initial_value_to_solver_units(
591                    exec_state,
592                    args.source_range,
593                    "edited segment fixed constraint value",
594                )?;
595                optional_constraints.push(ezpz::Constraint::Fixed(
596                    center_x_var.id.to_constraint_id(args.source_range)?,
597                    x_initial_value.n,
598                ));
599            }
600            if let Some(center_y_var) = center_y_value.as_sketch_var() {
601                let y_initial_value = center_y_var.initial_value_to_solver_units(
602                    exec_state,
603                    args.source_range,
604                    "edited segment fixed constraint value",
605                )?;
606                optional_constraints.push(ezpz::Constraint::Fixed(
607                    center_y_var.id.to_constraint_id(args.source_range)?,
608                    y_initial_value.n,
609                ));
610            }
611        }
612        optional_constraints
613    };
614
615    // Build the implicit arc constraint.
616    let range = args.source_range;
617    let constraint = ezpz::Constraint::Arc(ezpz::datatypes::inputs::DatumCircularArc {
618        center: ezpz::datatypes::inputs::DatumPoint::new_xy(
619            center_x.to_constraint_id(range)?,
620            center_y.to_constraint_id(range)?,
621        ),
622        start: ezpz::datatypes::inputs::DatumPoint::new_xy(
623            start_x.to_constraint_id(range)?,
624            start_y.to_constraint_id(range)?,
625        ),
626        end: ezpz::datatypes::inputs::DatumPoint::new_xy(
627            end_x.to_constraint_id(range)?,
628            end_y.to_constraint_id(range)?,
629        ),
630    });
631
632    let Some(sketch_state) = exec_state.sketch_block_mut() else {
633        return Err(KclError::new_semantic(KclErrorDetails::new(
634            "arc() can only be used inside a sketch block".to_owned(),
635            vec![args.source_range],
636        )));
637    };
638    // Save the segment to be sent to the engine after solving.
639    sketch_state.needed_by_engine.push(segment.clone());
640    // Save the constraint to be used for solving.
641    sketch_state.solver_constraints.push(constraint);
642    // The constraint isn't added to scene objects since it's implicit in the
643    // arc segment. You cannot have an arc without it.
644
645    #[cfg(feature = "artifact-graph")]
646    sketch_state.solver_optional_constraints.extend(optional_constraints);
647
648    let meta = segment.meta.clone();
649    let abstract_segment = AbstractSegment {
650        repr: SegmentRepr::Unsolved {
651            segment: Box::new(segment),
652        },
653        meta,
654    };
655    Ok(KclValue::Segment {
656        value: Box::new(abstract_segment),
657    })
658}
659
660pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
661    let start: Vec<KclValue> = args.get_kw_arg("start", &RuntimeType::point2d(), exec_state)?;
662    let center: Vec<KclValue> = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
663    let construction_opt = args.get_kw_arg_opt("construction", &RuntimeType::bool(), exec_state)?;
664    let construction: bool = construction_opt.unwrap_or(false);
665    let construction_ctor = construction_opt;
666
667    let [start_x_value, start_y_value]: [KclValue; 2] = start.try_into().map_err(|_| {
668        KclError::new_semantic(KclErrorDetails::new(
669            "start must be a 2D point".to_owned(),
670            vec![args.source_range],
671        ))
672    })?;
673    let [center_x_value, center_y_value]: [KclValue; 2] = center.try_into().map_err(|_| {
674        KclError::new_semantic(KclErrorDetails::new(
675            "center must be a 2D point".to_owned(),
676            vec![args.source_range],
677        ))
678    })?;
679
680    let Some(UnsolvedExpr::Unknown(start_x)) = start_x_value.as_unsolved_expr() else {
681        return Err(KclError::new_semantic(KclErrorDetails::new(
682            "start x must be a sketch var".to_owned(),
683            vec![args.source_range],
684        )));
685    };
686    let Some(UnsolvedExpr::Unknown(start_y)) = start_y_value.as_unsolved_expr() else {
687        return Err(KclError::new_semantic(KclErrorDetails::new(
688            "start y must be a sketch var".to_owned(),
689            vec![args.source_range],
690        )));
691    };
692    let Some(UnsolvedExpr::Unknown(center_x)) = center_x_value.as_unsolved_expr() else {
693        return Err(KclError::new_semantic(KclErrorDetails::new(
694            "center x must be a sketch var".to_owned(),
695            vec![args.source_range],
696        )));
697    };
698    let Some(UnsolvedExpr::Unknown(center_y)) = center_y_value.as_unsolved_expr() else {
699        return Err(KclError::new_semantic(KclErrorDetails::new(
700            "center y must be a sketch var".to_owned(),
701            vec![args.source_range],
702        )));
703    };
704
705    let ctor = CircleCtor {
706        start: Point2d {
707            x: start_x_value.to_sketch_expr().ok_or_else(|| {
708                KclError::new_semantic(KclErrorDetails::new(
709                    "unable to convert numeric type to suffix".to_owned(),
710                    vec![args.source_range],
711                ))
712            })?,
713            y: start_y_value.to_sketch_expr().ok_or_else(|| {
714                KclError::new_semantic(KclErrorDetails::new(
715                    "unable to convert numeric type to suffix".to_owned(),
716                    vec![args.source_range],
717                ))
718            })?,
719        },
720        center: Point2d {
721            x: center_x_value.to_sketch_expr().ok_or_else(|| {
722                KclError::new_semantic(KclErrorDetails::new(
723                    "unable to convert numeric type to suffix".to_owned(),
724                    vec![args.source_range],
725                ))
726            })?,
727            y: center_y_value.to_sketch_expr().ok_or_else(|| {
728                KclError::new_semantic(KclErrorDetails::new(
729                    "unable to convert numeric type to suffix".to_owned(),
730                    vec![args.source_range],
731                ))
732            })?,
733        },
734        construction: construction_ctor,
735    };
736
737    // Order of ID generation is important.
738    let start_object_id = exec_state.next_object_id();
739    let center_object_id = exec_state.next_object_id();
740    let circle_object_id = exec_state.next_object_id();
741    let segment = UnsolvedSegment {
742        id: exec_state.next_uuid(),
743        object_id: circle_object_id,
744        kind: UnsolvedSegmentKind::Circle {
745            start: [UnsolvedExpr::Unknown(start_x), UnsolvedExpr::Unknown(start_y)],
746            center: [UnsolvedExpr::Unknown(center_x), UnsolvedExpr::Unknown(center_y)],
747            ctor: Box::new(ctor),
748            start_object_id,
749            center_object_id,
750            construction,
751        },
752        tag: None,
753        node_path: args.node_path.clone(),
754        meta: vec![args.source_range.into()],
755    };
756    #[cfg(feature = "artifact-graph")]
757    let optional_constraints = {
758        let start_object_id =
759            exec_state.add_placeholder_scene_object(start_object_id, args.source_range, args.node_path.clone());
760        let center_object_id =
761            exec_state.add_placeholder_scene_object(center_object_id, args.source_range, args.node_path.clone());
762        let circle_object_id =
763            exec_state.add_placeholder_scene_object(circle_object_id, args.source_range, args.node_path.clone());
764
765        let mut optional_constraints = Vec::new();
766        if exec_state.segment_ids_edited_contains(&start_object_id)
767            || exec_state.segment_ids_edited_contains(&circle_object_id)
768        {
769            if let Some(start_x_var) = start_x_value.as_sketch_var() {
770                let x_initial_value = start_x_var.initial_value_to_solver_units(
771                    exec_state,
772                    args.source_range,
773                    "edited segment fixed constraint value",
774                )?;
775                optional_constraints.push(ezpz::Constraint::Fixed(
776                    start_x_var.id.to_constraint_id(args.source_range)?,
777                    x_initial_value.n,
778                ));
779            }
780            if let Some(start_y_var) = start_y_value.as_sketch_var() {
781                let y_initial_value = start_y_var.initial_value_to_solver_units(
782                    exec_state,
783                    args.source_range,
784                    "edited segment fixed constraint value",
785                )?;
786                optional_constraints.push(ezpz::Constraint::Fixed(
787                    start_y_var.id.to_constraint_id(args.source_range)?,
788                    y_initial_value.n,
789                ));
790            }
791        }
792        if exec_state.segment_ids_edited_contains(&center_object_id)
793            || exec_state.segment_ids_edited_contains(&circle_object_id)
794        {
795            if let Some(center_x_var) = center_x_value.as_sketch_var() {
796                let x_initial_value = center_x_var.initial_value_to_solver_units(
797                    exec_state,
798                    args.source_range,
799                    "edited segment fixed constraint value",
800                )?;
801                optional_constraints.push(ezpz::Constraint::Fixed(
802                    center_x_var.id.to_constraint_id(args.source_range)?,
803                    x_initial_value.n,
804                ));
805            }
806            if let Some(center_y_var) = center_y_value.as_sketch_var() {
807                let y_initial_value = center_y_var.initial_value_to_solver_units(
808                    exec_state,
809                    args.source_range,
810                    "edited segment fixed constraint value",
811                )?;
812                optional_constraints.push(ezpz::Constraint::Fixed(
813                    center_y_var.id.to_constraint_id(args.source_range)?,
814                    y_initial_value.n,
815                ));
816            }
817        }
818        optional_constraints
819    };
820
821    let Some(sketch_state) = exec_state.sketch_block_mut() else {
822        return Err(KclError::new_semantic(KclErrorDetails::new(
823            "circle() can only be used inside a sketch block".to_owned(),
824            vec![args.source_range],
825        )));
826    };
827    // Save the segment to be sent to the engine after solving.
828    sketch_state.needed_by_engine.push(segment.clone());
829
830    #[cfg(feature = "artifact-graph")]
831    sketch_state.solver_optional_constraints.extend(optional_constraints);
832
833    let meta = segment.meta.clone();
834    let abstract_segment = AbstractSegment {
835        repr: SegmentRepr::Unsolved {
836            segment: Box::new(segment),
837        },
838        meta,
839    };
840    Ok(KclValue::Segment {
841        value: Box::new(abstract_segment),
842    })
843}
844
845pub async fn coincident(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
846    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
847        "points",
848        &RuntimeType::Array(
849            Box::new(RuntimeType::Union(vec![RuntimeType::segment(), RuntimeType::point2d()])),
850            ArrayLen::Known(2),
851        ),
852        exec_state,
853    )?;
854    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
855        KclError::new_semantic(KclErrorDetails::new(
856            "must have two input points".to_owned(),
857            vec![args.source_range],
858        ))
859    })?;
860
861    let range = args.source_range;
862    match (&point0, &point1) {
863        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
864            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
865                return Err(KclError::new_semantic(KclErrorDetails::new(
866                    "first point must be an unsolved segment".to_owned(),
867                    vec![args.source_range],
868                )));
869            };
870            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
871                return Err(KclError::new_semantic(KclErrorDetails::new(
872                    "second point must be an unsolved segment".to_owned(),
873                    vec![args.source_range],
874                )));
875            };
876            match (&unsolved0.kind, &unsolved1.kind) {
877                (
878                    UnsolvedSegmentKind::Point { position: pos0, .. },
879                    UnsolvedSegmentKind::Point { position: pos1, .. },
880                ) => {
881                    let p0_x = &pos0[0];
882                    let p0_y = &pos0[1];
883                    match (p0_x, p0_y) {
884                        (UnsolvedExpr::Unknown(p0_x), UnsolvedExpr::Unknown(p0_y)) => {
885                            let p1_x = &pos1[0];
886                            let p1_y = &pos1[1];
887                            match (p1_x, p1_y) {
888                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
889                                    let constraint = SolverConstraint::PointsCoincident(
890                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
891                                            p0_x.to_constraint_id(range)?,
892                                            p0_y.to_constraint_id(range)?,
893                                        ),
894                                        ezpz::datatypes::inputs::DatumPoint::new_xy(
895                                            p1_x.to_constraint_id(range)?,
896                                            p1_y.to_constraint_id(range)?,
897                                        ),
898                                    );
899                                    #[cfg(feature = "artifact-graph")]
900                                    let constraint_id = exec_state.next_object_id();
901                                    // Save the constraint to be used for solving.
902                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
903                                        return Err(KclError::new_semantic(KclErrorDetails::new(
904                                            "coincident() can only be used inside a sketch block".to_owned(),
905                                            vec![args.source_range],
906                                        )));
907                                    };
908                                    sketch_state.solver_constraints.push(constraint);
909                                    #[cfg(feature = "artifact-graph")]
910                                    {
911                                        let constraint = crate::front::Constraint::Coincident(Coincident {
912                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
913                                        });
914                                        sketch_state.sketch_constraints.push(constraint_id);
915                                        track_constraint(constraint_id, constraint, exec_state, &args);
916                                    }
917                                    Ok(KclValue::none())
918                                }
919                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
920                                    let p1_x = KclValue::Number {
921                                        value: p1_x.n,
922                                        ty: p1_x.ty,
923                                        meta: vec![args.source_range.into()],
924                                    };
925                                    let p1_y = KclValue::Number {
926                                        value: p1_y.n,
927                                        ty: p1_y.ty,
928                                        meta: vec![args.source_range.into()],
929                                    };
930                                    let (constraint_x, constraint_y) =
931                                        coincident_constraints_fixed(*p0_x, *p0_y, &p1_x, &p1_y, exec_state, &args)?;
932
933                                    #[cfg(feature = "artifact-graph")]
934                                    let constraint_id = exec_state.next_object_id();
935                                    // Save the constraint to be used for solving.
936                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
937                                        return Err(KclError::new_semantic(KclErrorDetails::new(
938                                            "coincident() can only be used inside a sketch block".to_owned(),
939                                            vec![args.source_range],
940                                        )));
941                                    };
942                                    sketch_state.solver_constraints.push(constraint_x);
943                                    sketch_state.solver_constraints.push(constraint_y);
944                                    #[cfg(feature = "artifact-graph")]
945                                    {
946                                        let constraint = crate::front::Constraint::Coincident(Coincident {
947                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
948                                        });
949                                        sketch_state.sketch_constraints.push(constraint_id);
950                                        track_constraint(constraint_id, constraint, exec_state, &args);
951                                    }
952                                    Ok(KclValue::none())
953                                }
954                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
955                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
956                                    // TODO: sketch-api: unimplemented
957                                    Err(KclError::new_semantic(KclErrorDetails::new(
958                                        "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(),
959                                        vec![args.source_range],
960                                    )))
961                                }
962                            }
963                        }
964                        (UnsolvedExpr::Known(p0_x), UnsolvedExpr::Known(p0_y)) => {
965                            let p1_x = &pos1[0];
966                            let p1_y = &pos1[1];
967                            match (p1_x, p1_y) {
968                                (UnsolvedExpr::Unknown(p1_x), UnsolvedExpr::Unknown(p1_y)) => {
969                                    let p0_x = KclValue::Number {
970                                        value: p0_x.n,
971                                        ty: p0_x.ty,
972                                        meta: vec![args.source_range.into()],
973                                    };
974                                    let p0_y = KclValue::Number {
975                                        value: p0_y.n,
976                                        ty: p0_y.ty,
977                                        meta: vec![args.source_range.into()],
978                                    };
979                                    let (constraint_x, constraint_y) =
980                                        coincident_constraints_fixed(*p1_x, *p1_y, &p0_x, &p0_y, exec_state, &args)?;
981
982                                    #[cfg(feature = "artifact-graph")]
983                                    let constraint_id = exec_state.next_object_id();
984                                    // Save the constraint to be used for solving.
985                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
986                                        return Err(KclError::new_semantic(KclErrorDetails::new(
987                                            "coincident() can only be used inside a sketch block".to_owned(),
988                                            vec![args.source_range],
989                                        )));
990                                    };
991                                    sketch_state.solver_constraints.push(constraint_x);
992                                    sketch_state.solver_constraints.push(constraint_y);
993                                    #[cfg(feature = "artifact-graph")]
994                                    {
995                                        let constraint = crate::front::Constraint::Coincident(Coincident {
996                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
997                                        });
998                                        sketch_state.sketch_constraints.push(constraint_id);
999                                        track_constraint(constraint_id, constraint, exec_state, &args);
1000                                    }
1001                                    Ok(KclValue::none())
1002                                }
1003                                (UnsolvedExpr::Known(p1_x), UnsolvedExpr::Known(p1_y)) => {
1004                                    if *p0_x != *p1_x || *p0_y != *p1_y {
1005                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1006                                            "Coincident constraint between two fixed points failed since coordinates differ"
1007                                                .to_owned(),
1008                                            vec![args.source_range],
1009                                        )));
1010                                    }
1011                                    Ok(KclValue::none())
1012                                }
1013                                (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1014                                | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1015                                    // TODO: sketch-api: unimplemented
1016                                    Err(KclError::new_semantic(KclErrorDetails::new(
1017                                        "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(),
1018                                        vec![args.source_range],
1019                                    )))
1020                                }
1021                            }
1022                        }
1023                        (UnsolvedExpr::Known(_), UnsolvedExpr::Unknown(_))
1024                        | (UnsolvedExpr::Unknown(_), UnsolvedExpr::Known(_)) => {
1025                            // The segment is a point with one sketch var.
1026                            Err(KclError::new_semantic(KclErrorDetails::new(
1027                                "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(),
1028                                vec![args.source_range],
1029                            )))
1030                        }
1031                    }
1032                }
1033                // Point-Line or Line-Point case: create perpendicular distance constraint with distance 0
1034                (
1035                    UnsolvedSegmentKind::Point {
1036                        position: point_pos, ..
1037                    },
1038                    UnsolvedSegmentKind::Line {
1039                        start: line_start,
1040                        end: line_end,
1041                        ..
1042                    },
1043                )
1044                | (
1045                    UnsolvedSegmentKind::Line {
1046                        start: line_start,
1047                        end: line_end,
1048                        ..
1049                    },
1050                    UnsolvedSegmentKind::Point {
1051                        position: point_pos, ..
1052                    },
1053                ) => {
1054                    let point_x = &point_pos[0];
1055                    let point_y = &point_pos[1];
1056                    match (point_x, point_y) {
1057                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1058                            // Extract line start and end coordinates
1059                            let (start_x, start_y) = (&line_start[0], &line_start[1]);
1060                            let (end_x, end_y) = (&line_end[0], &line_end[1]);
1061
1062                            match (start_x, start_y, end_x, end_y) {
1063                                (
1064                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1065                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1066                                ) => {
1067                                    let point = DatumPoint::new_xy(
1068                                        point_x.to_constraint_id(range)?,
1069                                        point_y.to_constraint_id(range)?,
1070                                    );
1071                                    let line_segment = DatumLineSegment::new(
1072                                        DatumPoint::new_xy(sx.to_constraint_id(range)?, sy.to_constraint_id(range)?),
1073                                        DatumPoint::new_xy(ex.to_constraint_id(range)?, ey.to_constraint_id(range)?),
1074                                    );
1075                                    let constraint = SolverConstraint::PointLineDistance(point, line_segment, 0.0);
1076
1077                                    #[cfg(feature = "artifact-graph")]
1078                                    let constraint_id = exec_state.next_object_id();
1079
1080                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1081                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1082                                            "coincident() can only be used inside a sketch block".to_owned(),
1083                                            vec![args.source_range],
1084                                        )));
1085                                    };
1086                                    sketch_state.solver_constraints.push(constraint);
1087                                    #[cfg(feature = "artifact-graph")]
1088                                    {
1089                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1090                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1091                                        });
1092                                        sketch_state.sketch_constraints.push(constraint_id);
1093                                        track_constraint(constraint_id, constraint, exec_state, &args);
1094                                    }
1095                                    Ok(KclValue::none())
1096                                }
1097                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1098                                    "Line segment endpoints must be sketch variables for point-segment coincident constraint".to_owned(),
1099                                    vec![args.source_range],
1100                                ))),
1101                            }
1102                        }
1103                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1104                            "Point coordinates must be sketch variables for point-segment coincident constraint"
1105                                .to_owned(),
1106                            vec![args.source_range],
1107                        ))),
1108                    }
1109                }
1110                // Point-Arc or Arc-Point case: create PointArcCoincident constraint
1111                (
1112                    UnsolvedSegmentKind::Point {
1113                        position: point_pos, ..
1114                    },
1115                    UnsolvedSegmentKind::Arc {
1116                        start: arc_start,
1117                        end: arc_end,
1118                        center: arc_center,
1119                        ..
1120                    },
1121                )
1122                | (
1123                    UnsolvedSegmentKind::Arc {
1124                        start: arc_start,
1125                        end: arc_end,
1126                        center: arc_center,
1127                        ..
1128                    },
1129                    UnsolvedSegmentKind::Point {
1130                        position: point_pos, ..
1131                    },
1132                ) => {
1133                    let point_x = &point_pos[0];
1134                    let point_y = &point_pos[1];
1135                    match (point_x, point_y) {
1136                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1137                            // Extract arc center, start, and end coordinates
1138                            let (center_x, center_y) = (&arc_center[0], &arc_center[1]);
1139                            let (start_x, start_y) = (&arc_start[0], &arc_start[1]);
1140                            let (end_x, end_y) = (&arc_end[0], &arc_end[1]);
1141
1142                            match (center_x, center_y, start_x, start_y, end_x, end_y) {
1143                                (
1144                                    UnsolvedExpr::Unknown(cx), UnsolvedExpr::Unknown(cy),
1145                                    UnsolvedExpr::Unknown(sx), UnsolvedExpr::Unknown(sy),
1146                                    UnsolvedExpr::Unknown(ex), UnsolvedExpr::Unknown(ey),
1147                                ) => {
1148                                    let point = DatumPoint::new_xy(
1149                                        point_x.to_constraint_id(range)?,
1150                                        point_y.to_constraint_id(range)?,
1151                                    );
1152                                    let circular_arc = DatumCircularArc {
1153                                        center: DatumPoint::new_xy(
1154                                            cx.to_constraint_id(range)?,
1155                                            cy.to_constraint_id(range)?,
1156                                        ),
1157                                        start: DatumPoint::new_xy(
1158                                            sx.to_constraint_id(range)?,
1159                                            sy.to_constraint_id(range)?,
1160                                        ),
1161                                        end: DatumPoint::new_xy(
1162                                            ex.to_constraint_id(range)?,
1163                                            ey.to_constraint_id(range)?,
1164                                        ),
1165                                    };
1166                                    let constraint = SolverConstraint::PointArcCoincident(circular_arc, point);
1167
1168                                    #[cfg(feature = "artifact-graph")]
1169                                    let constraint_id = exec_state.next_object_id();
1170
1171                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1172                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1173                                            "coincident() can only be used inside a sketch block".to_owned(),
1174                                            vec![args.source_range],
1175                                        )));
1176                                    };
1177                                    sketch_state.solver_constraints.push(constraint);
1178                                    #[cfg(feature = "artifact-graph")]
1179                                    {
1180                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1181                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1182                                        });
1183                                        sketch_state.sketch_constraints.push(constraint_id);
1184                                        track_constraint(constraint_id, constraint, exec_state, &args);
1185                                    }
1186                                    Ok(KclValue::none())
1187                                }
1188                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1189                                    "Arc center, start, and end points must be sketch variables for point-arc coincident constraint".to_owned(),
1190                                    vec![args.source_range],
1191                                ))),
1192                            }
1193                        }
1194                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1195                            "Point coordinates must be sketch variables for point-arc coincident constraint".to_owned(),
1196                            vec![args.source_range],
1197                        ))),
1198                    }
1199                }
1200                // Point-Circle or Circle-Point case: constrain point-to-center distance
1201                // to equal the circle radius.
1202                (
1203                    UnsolvedSegmentKind::Point {
1204                        position: point_pos, ..
1205                    },
1206                    UnsolvedSegmentKind::Circle {
1207                        start: circle_start,
1208                        center: circle_center,
1209                        ..
1210                    },
1211                )
1212                | (
1213                    UnsolvedSegmentKind::Circle {
1214                        start: circle_start,
1215                        center: circle_center,
1216                        ..
1217                    },
1218                    UnsolvedSegmentKind::Point {
1219                        position: point_pos, ..
1220                    },
1221                ) => {
1222                    let point_x = &point_pos[0];
1223                    let point_y = &point_pos[1];
1224                    match (point_x, point_y) {
1225                        (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1226                            // Extract circle center and start coordinates.
1227                            let (center_x, center_y) = (&circle_center[0], &circle_center[1]);
1228                            let (start_x, start_y) = (&circle_start[0], &circle_start[1]);
1229
1230                            match (center_x, center_y, start_x, start_y) {
1231                                (
1232                                    UnsolvedExpr::Unknown(cx),
1233                                    UnsolvedExpr::Unknown(cy),
1234                                    UnsolvedExpr::Unknown(sx),
1235                                    UnsolvedExpr::Unknown(sy),
1236                                ) => {
1237                                    let point_radius_line = DatumLineSegment::new(
1238                                        DatumPoint::new_xy(
1239                                            cx.to_constraint_id(range)?,
1240                                            cy.to_constraint_id(range)?,
1241                                        ),
1242                                        DatumPoint::new_xy(
1243                                            point_x.to_constraint_id(range)?,
1244                                            point_y.to_constraint_id(range)?,
1245                                        ),
1246                                    );
1247                                    let circle_radius_line = DatumLineSegment::new(
1248                                        DatumPoint::new_xy(
1249                                            cx.to_constraint_id(range)?,
1250                                            cy.to_constraint_id(range)?,
1251                                        ),
1252                                        DatumPoint::new_xy(
1253                                            sx.to_constraint_id(range)?,
1254                                            sy.to_constraint_id(range)?,
1255                                        ),
1256                                    );
1257                                    let constraint =
1258                                        SolverConstraint::LinesEqualLength(point_radius_line, circle_radius_line);
1259
1260                                    #[cfg(feature = "artifact-graph")]
1261                                    let constraint_id = exec_state.next_object_id();
1262
1263                                    let Some(sketch_state) = exec_state.sketch_block_mut() else {
1264                                        return Err(KclError::new_semantic(KclErrorDetails::new(
1265                                            "coincident() can only be used inside a sketch block".to_owned(),
1266                                            vec![args.source_range],
1267                                        )));
1268                                    };
1269                                    sketch_state.solver_constraints.push(constraint);
1270                                    #[cfg(feature = "artifact-graph")]
1271                                    {
1272                                        let constraint = crate::front::Constraint::Coincident(Coincident {
1273                                            segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1274                                        });
1275                                        sketch_state.sketch_constraints.push(constraint_id);
1276                                        track_constraint(constraint_id, constraint, exec_state, &args);
1277                                    }
1278                                    Ok(KclValue::none())
1279                                }
1280                                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1281                                    "Circle start and center points must be sketch variables for point-circle coincident constraint".to_owned(),
1282                                    vec![args.source_range],
1283                                ))),
1284                            }
1285                        }
1286                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1287                            "Point coordinates must be sketch variables for point-circle coincident constraint"
1288                                .to_owned(),
1289                            vec![args.source_range],
1290                        ))),
1291                    }
1292                }
1293                // Line-Line case: create parallel constraint and perpendicular distance of zero
1294                (
1295                    UnsolvedSegmentKind::Line {
1296                        start: line0_start,
1297                        end: line0_end,
1298                        ..
1299                    },
1300                    UnsolvedSegmentKind::Line {
1301                        start: line1_start,
1302                        end: line1_end,
1303                        ..
1304                    },
1305                ) => {
1306                    // Extract line coordinates
1307                    let (line0_start_x, line0_start_y) = (&line0_start[0], &line0_start[1]);
1308                    let (line0_end_x, line0_end_y) = (&line0_end[0], &line0_end[1]);
1309                    let (line1_start_x, line1_start_y) = (&line1_start[0], &line1_start[1]);
1310                    let (line1_end_x, line1_end_y) = (&line1_end[0], &line1_end[1]);
1311
1312                    match (
1313                        line0_start_x,
1314                        line0_start_y,
1315                        line0_end_x,
1316                        line0_end_y,
1317                        line1_start_x,
1318                        line1_start_y,
1319                        line1_end_x,
1320                        line1_end_y,
1321                    ) {
1322                        (
1323                            UnsolvedExpr::Unknown(l0_sx),
1324                            UnsolvedExpr::Unknown(l0_sy),
1325                            UnsolvedExpr::Unknown(l0_ex),
1326                            UnsolvedExpr::Unknown(l0_ey),
1327                            UnsolvedExpr::Unknown(l1_sx),
1328                            UnsolvedExpr::Unknown(l1_sy),
1329                            UnsolvedExpr::Unknown(l1_ex),
1330                            UnsolvedExpr::Unknown(l1_ey),
1331                        ) => {
1332                            // Create line segments for the solver
1333                            let line0_segment = DatumLineSegment::new(
1334                                DatumPoint::new_xy(l0_sx.to_constraint_id(range)?, l0_sy.to_constraint_id(range)?),
1335                                DatumPoint::new_xy(l0_ex.to_constraint_id(range)?, l0_ey.to_constraint_id(range)?),
1336                            );
1337                            let line1_segment = DatumLineSegment::new(
1338                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?),
1339                                DatumPoint::new_xy(l1_ex.to_constraint_id(range)?, l1_ey.to_constraint_id(range)?),
1340                            );
1341
1342                            // Create parallel constraint
1343                            let parallel_constraint =
1344                                SolverConstraint::LinesAtAngle(line0_segment, line1_segment, AngleKind::Parallel);
1345
1346                            // Create perpendicular distance constraint from first line to start point of second line
1347                            let point_on_line1 =
1348                                DatumPoint::new_xy(l1_sx.to_constraint_id(range)?, l1_sy.to_constraint_id(range)?);
1349                            let distance_constraint =
1350                                SolverConstraint::PointLineDistance(point_on_line1, line0_segment, 0.0);
1351
1352                            #[cfg(feature = "artifact-graph")]
1353                            let constraint_id = exec_state.next_object_id();
1354
1355                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1356                                return Err(KclError::new_semantic(KclErrorDetails::new(
1357                                    "coincident() can only be used inside a sketch block".to_owned(),
1358                                    vec![args.source_range],
1359                                )));
1360                            };
1361                            // Push both constraints to achieve collinearity
1362                            sketch_state.solver_constraints.push(parallel_constraint);
1363                            sketch_state.solver_constraints.push(distance_constraint);
1364                            #[cfg(feature = "artifact-graph")]
1365                            {
1366                                let constraint = crate::front::Constraint::Coincident(Coincident {
1367                                    segments: vec![unsolved0.object_id.into(), unsolved1.object_id.into()],
1368                                });
1369                                sketch_state.sketch_constraints.push(constraint_id);
1370                                track_constraint(constraint_id, constraint, exec_state, &args);
1371                            }
1372                            Ok(KclValue::none())
1373                        }
1374                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1375                            "Line segment endpoints must be sketch variables for line-line coincident constraint"
1376                                .to_owned(),
1377                            vec![args.source_range],
1378                        ))),
1379                    }
1380                }
1381                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1382                    format!(
1383                        "coincident supports point-point, point-segment, or segment-segment; found {:?} and {:?}",
1384                        &unsolved0.kind, &unsolved1.kind
1385                    ),
1386                    vec![args.source_range],
1387                ))),
1388            }
1389        }
1390        // One argument is a Segment and the other is a Point2d literal.
1391        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1392        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1393            let Some(pt) = <[TyF64; 2]>::from_kcl_val(point2d) else {
1394                return Err(KclError::new_semantic(KclErrorDetails::new(
1395                    "Expected a Segment or Point2d (e.g. [1mm, 2mm])".to_owned(),
1396                    vec![args.source_range],
1397                )));
1398            };
1399            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1400                return Err(KclError::new_semantic(KclErrorDetails::new(
1401                    "segment must be an unsolved segment".to_owned(),
1402                    vec![args.source_range],
1403                )));
1404            };
1405            match &unsolved.kind {
1406                UnsolvedSegmentKind::Point { position, .. } => {
1407                    let p_x = &position[0];
1408                    let p_y = &position[1];
1409                    match (p_x, p_y) {
1410                        (UnsolvedExpr::Unknown(p_x), UnsolvedExpr::Unknown(p_y)) => {
1411                            let pt_x = KclValue::Number {
1412                                value: pt[0].n,
1413                                ty: pt[0].ty,
1414                                meta: vec![args.source_range.into()],
1415                            };
1416                            let pt_y = KclValue::Number {
1417                                value: pt[1].n,
1418                                ty: pt[1].ty,
1419                                meta: vec![args.source_range.into()],
1420                            };
1421                            let (constraint_x, constraint_y) =
1422                                coincident_constraints_fixed(*p_x, *p_y, &pt_x, &pt_y, exec_state, &args)?;
1423
1424                            #[cfg(feature = "artifact-graph")]
1425                            let constraint_id = exec_state.next_object_id();
1426                            #[cfg(feature = "artifact-graph")]
1427                            let coincident_segments = coincident_segments_for_segment_and_point2d(
1428                                unsolved.object_id,
1429                                point2d,
1430                                matches!((&point0, &point1), (KclValue::Segment { .. }, _)),
1431                            );
1432                            let Some(sketch_state) = exec_state.sketch_block_mut() else {
1433                                return Err(KclError::new_semantic(KclErrorDetails::new(
1434                                    "coincident() can only be used inside a sketch block".to_owned(),
1435                                    vec![args.source_range],
1436                                )));
1437                            };
1438                            sketch_state.solver_constraints.push(constraint_x);
1439                            sketch_state.solver_constraints.push(constraint_y);
1440                            #[cfg(feature = "artifact-graph")]
1441                            {
1442                                let constraint = crate::front::Constraint::Coincident(Coincident {
1443                                    segments: coincident_segments,
1444                                });
1445                                sketch_state.sketch_constraints.push(constraint_id);
1446                                track_constraint(constraint_id, constraint, exec_state, &args);
1447                            }
1448                            Ok(KclValue::none())
1449                        }
1450                        (UnsolvedExpr::Known(known_x), UnsolvedExpr::Known(known_y)) => {
1451                            let pt_x_val = normalize_to_solver_distance_unit(
1452                                &KclValue::Number {
1453                                    value: pt[0].n,
1454                                    ty: pt[0].ty,
1455                                    meta: vec![args.source_range.into()],
1456                                },
1457                                args.source_range,
1458                                exec_state,
1459                                "coincident constraint value",
1460                            )?;
1461                            let pt_y_val = normalize_to_solver_distance_unit(
1462                                &KclValue::Number {
1463                                    value: pt[1].n,
1464                                    ty: pt[1].ty,
1465                                    meta: vec![args.source_range.into()],
1466                                },
1467                                args.source_range,
1468                                exec_state,
1469                                "coincident constraint value",
1470                            )?;
1471                            let Some(pt_x) = pt_x_val.as_ty_f64() else {
1472                                return Err(KclError::new_semantic(KclErrorDetails::new(
1473                                    "Expected number for Point2d x coordinate".to_owned(),
1474                                    vec![args.source_range],
1475                                )));
1476                            };
1477                            let Some(pt_y) = pt_y_val.as_ty_f64() else {
1478                                return Err(KclError::new_semantic(KclErrorDetails::new(
1479                                    "Expected number for Point2d y coordinate".to_owned(),
1480                                    vec![args.source_range],
1481                                )));
1482                            };
1483                            let known_x_val = normalize_to_solver_distance_unit(
1484                                &KclValue::Number {
1485                                    value: known_x.n,
1486                                    ty: known_x.ty,
1487                                    meta: vec![args.source_range.into()],
1488                                },
1489                                args.source_range,
1490                                exec_state,
1491                                "coincident constraint value",
1492                            )?;
1493                            let Some(known_x_f) = known_x_val.as_ty_f64() else {
1494                                return Err(KclError::new_semantic(KclErrorDetails::new(
1495                                    "Expected number for known x coordinate".to_owned(),
1496                                    vec![args.source_range],
1497                                )));
1498                            };
1499                            let known_y_val = normalize_to_solver_distance_unit(
1500                                &KclValue::Number {
1501                                    value: known_y.n,
1502                                    ty: known_y.ty,
1503                                    meta: vec![args.source_range.into()],
1504                                },
1505                                args.source_range,
1506                                exec_state,
1507                                "coincident constraint value",
1508                            )?;
1509                            let Some(known_y_f) = known_y_val.as_ty_f64() else {
1510                                return Err(KclError::new_semantic(KclErrorDetails::new(
1511                                    "Expected number for known y coordinate".to_owned(),
1512                                    vec![args.source_range],
1513                                )));
1514                            };
1515                            if known_x_f.n != pt_x.n || known_y_f.n != pt_y.n {
1516                                return Err(KclError::new_semantic(KclErrorDetails::new(
1517                                    "Coincident constraint between two fixed points failed since coordinates differ"
1518                                        .to_owned(),
1519                                    vec![args.source_range],
1520                                )));
1521                            }
1522                            Ok(KclValue::none())
1523                        }
1524                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1525                            "Point coordinates must have consistent known/unknown status for coincident constraint"
1526                                .to_owned(),
1527                            vec![args.source_range],
1528                        ))),
1529                    }
1530                }
1531                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1532                    "A Point2d can only be constrained coincident with a point segment, not a line or arc".to_owned(),
1533                    vec![args.source_range],
1534                ))),
1535            }
1536        }
1537        // Both arguments are Point2d literals -- just verify equality.
1538        _ => {
1539            let pt0 = <[TyF64; 2]>::from_kcl_val(&point0);
1540            let pt1 = <[TyF64; 2]>::from_kcl_val(&point1);
1541            match (pt0, pt1) {
1542                (Some(a), Some(b)) => {
1543                    // Normalize both to solver units and compare.
1544                    let a_x = normalize_to_solver_distance_unit(
1545                        &KclValue::Number {
1546                            value: a[0].n,
1547                            ty: a[0].ty,
1548                            meta: vec![args.source_range.into()],
1549                        },
1550                        args.source_range,
1551                        exec_state,
1552                        "coincident constraint value",
1553                    )?;
1554                    let a_y = normalize_to_solver_distance_unit(
1555                        &KclValue::Number {
1556                            value: a[1].n,
1557                            ty: a[1].ty,
1558                            meta: vec![args.source_range.into()],
1559                        },
1560                        args.source_range,
1561                        exec_state,
1562                        "coincident constraint value",
1563                    )?;
1564                    let b_x = normalize_to_solver_distance_unit(
1565                        &KclValue::Number {
1566                            value: b[0].n,
1567                            ty: b[0].ty,
1568                            meta: vec![args.source_range.into()],
1569                        },
1570                        args.source_range,
1571                        exec_state,
1572                        "coincident constraint value",
1573                    )?;
1574                    let b_y = normalize_to_solver_distance_unit(
1575                        &KclValue::Number {
1576                            value: b[1].n,
1577                            ty: b[1].ty,
1578                            meta: vec![args.source_range.into()],
1579                        },
1580                        args.source_range,
1581                        exec_state,
1582                        "coincident constraint value",
1583                    )?;
1584                    if a_x.as_ty_f64().map(|v| v.n) != b_x.as_ty_f64().map(|v| v.n)
1585                        || a_y.as_ty_f64().map(|v| v.n) != b_y.as_ty_f64().map(|v| v.n)
1586                    {
1587                        return Err(KclError::new_semantic(KclErrorDetails::new(
1588                            "Coincident constraint between two fixed points failed since coordinates differ".to_owned(),
1589                            vec![args.source_range],
1590                        )));
1591                    }
1592                    Ok(KclValue::none())
1593                }
1594                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1595                    "All inputs must be Segments or Point2d values".to_owned(),
1596                    vec![args.source_range],
1597                ))),
1598            }
1599        }
1600    }
1601}
1602
1603#[cfg(feature = "artifact-graph")]
1604fn track_constraint(constraint_id: ObjectId, constraint: Constraint, exec_state: &mut ExecState, args: &Args) {
1605    let sketch_id = {
1606        let Some(sketch_state) = exec_state.sketch_block_mut() else {
1607            debug_assert!(false, "Constraint created outside a sketch block");
1608            return;
1609        };
1610        sketch_state.sketch_id
1611    };
1612    let Some(sketch_id) = sketch_id else {
1613        debug_assert!(false, "Constraint created without a sketch id");
1614        return;
1615    };
1616    let artifact_id = exec_state.next_artifact_id();
1617    exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
1618        id: artifact_id,
1619        sketch_id,
1620        constraint_id,
1621        constraint_type: SketchBlockConstraintType::from(&constraint),
1622        code_ref: CodeRef::placeholder(args.source_range),
1623    }));
1624    exec_state.add_scene_object(
1625        Object {
1626            id: constraint_id,
1627            kind: ObjectKind::Constraint { constraint },
1628            label: Default::default(),
1629            comments: Default::default(),
1630            artifact_id,
1631            source: SourceRef::new(args.source_range, args.node_path.clone()),
1632        },
1633        args.source_range,
1634    );
1635}
1636
1637/// Order of points has been erased when calling this function.
1638fn coincident_constraints_fixed(
1639    p0_x: SketchVarId,
1640    p0_y: SketchVarId,
1641    p1_x: &KclValue,
1642    p1_y: &KclValue,
1643    exec_state: &mut ExecState,
1644    args: &Args,
1645) -> Result<(ezpz::Constraint, ezpz::Constraint), KclError> {
1646    let p1_x_number_value =
1647        normalize_to_solver_distance_unit(p1_x, p1_x.into(), exec_state, "coincident constraint value")?;
1648    let p1_y_number_value =
1649        normalize_to_solver_distance_unit(p1_y, p1_y.into(), exec_state, "coincident constraint value")?;
1650    let Some(p1_x) = p1_x_number_value.as_ty_f64() else {
1651        let message = format!(
1652            "Expected number after coercion, but found {}",
1653            p1_x_number_value.human_friendly_type()
1654        );
1655        debug_assert!(false, "{}", &message);
1656        return Err(KclError::new_internal(KclErrorDetails::new(
1657            message,
1658            vec![args.source_range],
1659        )));
1660    };
1661    let Some(p1_y) = p1_y_number_value.as_ty_f64() else {
1662        let message = format!(
1663            "Expected number after coercion, but found {}",
1664            p1_y_number_value.human_friendly_type()
1665        );
1666        debug_assert!(false, "{}", &message);
1667        return Err(KclError::new_internal(KclErrorDetails::new(
1668            message,
1669            vec![args.source_range],
1670        )));
1671    };
1672    let constraint_x = SolverConstraint::Fixed(p0_x.to_constraint_id(args.source_range)?, p1_x.n);
1673    let constraint_y = SolverConstraint::Fixed(p0_y.to_constraint_id(args.source_range)?, p1_y.n);
1674    Ok((constraint_x, constraint_y))
1675}
1676
1677pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1678    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1679        "points",
1680        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
1681        exec_state,
1682    )?;
1683    let [point0, point1]: [KclValue; 2] = points.try_into().map_err(|_| {
1684        KclError::new_semantic(KclErrorDetails::new(
1685            "must have two input points".to_owned(),
1686            vec![args.source_range],
1687        ))
1688    })?;
1689
1690    match (&point0, &point1) {
1691        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1692            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1693                return Err(KclError::new_semantic(KclErrorDetails::new(
1694                    "first point must be an unsolved segment".to_owned(),
1695                    vec![args.source_range],
1696                )));
1697            };
1698            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1699                return Err(KclError::new_semantic(KclErrorDetails::new(
1700                    "second point must be an unsolved segment".to_owned(),
1701                    vec![args.source_range],
1702                )));
1703            };
1704            match (&unsolved0.kind, &unsolved1.kind) {
1705                (
1706                    UnsolvedSegmentKind::Point { position: pos0, .. },
1707                    UnsolvedSegmentKind::Point { position: pos1, .. },
1708                ) => {
1709                    // Both segments are points. Create a distance constraint
1710                    // between them.
1711                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
1712                        (
1713                            UnsolvedExpr::Unknown(p0_x),
1714                            UnsolvedExpr::Unknown(p0_y),
1715                            UnsolvedExpr::Unknown(p1_x),
1716                            UnsolvedExpr::Unknown(p1_y),
1717                        ) => {
1718                            // All coordinates are sketch vars. Proceed.
1719                            let sketch_constraint = SketchConstraint {
1720                                kind: SketchConstraintKind::Distance {
1721                                    points: [
1722                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1723                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
1724                                            object_id: unsolved0.object_id,
1725                                        }),
1726                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1727                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
1728                                            object_id: unsolved1.object_id,
1729                                        }),
1730                                    ],
1731                                },
1732                                meta: vec![args.source_range.into()],
1733                            };
1734                            Ok(KclValue::SketchConstraint {
1735                                value: Box::new(sketch_constraint),
1736                            })
1737                        }
1738                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1739                            "unimplemented: distance() arguments must be all sketch vars in all coordinates".to_owned(),
1740                            vec![args.source_range],
1741                        ))),
1742                    }
1743                }
1744                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1745                    "distance() arguments must be unsolved points".to_owned(),
1746                    vec![args.source_range],
1747                ))),
1748            }
1749        }
1750        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
1751        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
1752            if !point2d_is_origin(point2d) {
1753                return Err(KclError::new_semantic(KclErrorDetails::new(
1754                    "distance() Point2d arguments must be ORIGIN".to_owned(),
1755                    vec![args.source_range],
1756                )));
1757            }
1758
1759            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1760                return Err(KclError::new_semantic(KclErrorDetails::new(
1761                    "segment must be an unsolved segment".to_owned(),
1762                    vec![args.source_range],
1763                )));
1764            };
1765            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
1766                return Err(KclError::new_semantic(KclErrorDetails::new(
1767                    "distance() arguments must be unsolved points or ORIGIN".to_owned(),
1768                    vec![args.source_range],
1769                )));
1770            };
1771            match (&position[0], &position[1]) {
1772                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
1773                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1774                        vars: crate::front::Point2d {
1775                            x: *point_x,
1776                            y: *point_y,
1777                        },
1778                        object_id: unsolved.object_id,
1779                    });
1780                    let points = if matches!((&point0, &point1), (KclValue::Segment { .. }, _)) {
1781                        [point, ConstrainablePoint2dOrOrigin::Origin]
1782                    } else {
1783                        [ConstrainablePoint2dOrOrigin::Origin, point]
1784                    };
1785                    Ok(KclValue::SketchConstraint {
1786                        value: Box::new(SketchConstraint {
1787                            kind: SketchConstraintKind::Distance { points },
1788                            meta: vec![args.source_range.into()],
1789                        }),
1790                    })
1791                }
1792                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1793                    "unimplemented: distance() point arguments must be sketch vars in all coordinates".to_owned(),
1794                    vec![args.source_range],
1795                ))),
1796            }
1797        }
1798        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1799            "distance() arguments must be point segments or ORIGIN".to_owned(),
1800            vec![args.source_range],
1801        ))),
1802    }
1803}
1804
1805/// Helper function to create a radius or diameter constraint from a circular segment.
1806/// Used by both radius() and diameter() functions.
1807fn create_circular_radius_constraint(
1808    segment: KclValue,
1809    constraint_kind: fn([ConstrainablePoint2d; 2]) -> SketchConstraintKind,
1810    source_range: crate::SourceRange,
1811) -> Result<SketchConstraint, KclError> {
1812    // Create a dummy constraint to get its name for error messages
1813    let dummy_constraint = constraint_kind([
1814        ConstrainablePoint2d {
1815            vars: crate::front::Point2d {
1816                x: SketchVarId(0),
1817                y: SketchVarId(0),
1818            },
1819            object_id: ObjectId(0),
1820        },
1821        ConstrainablePoint2d {
1822            vars: crate::front::Point2d {
1823                x: SketchVarId(0),
1824                y: SketchVarId(0),
1825            },
1826            object_id: ObjectId(0),
1827        },
1828    ]);
1829    let function_name = dummy_constraint.name();
1830
1831    let KclValue::Segment { value: seg } = segment else {
1832        return Err(KclError::new_semantic(KclErrorDetails::new(
1833            format!("{}() argument must be a segment", function_name),
1834            vec![source_range],
1835        )));
1836    };
1837    let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
1838        return Err(KclError::new_semantic(KclErrorDetails::new(
1839            "segment must be unsolved".to_owned(),
1840            vec![source_range],
1841        )));
1842    };
1843    match &unsolved.kind {
1844        UnsolvedSegmentKind::Arc {
1845            center,
1846            start,
1847            center_object_id,
1848            start_object_id,
1849            ..
1850        }
1851        | UnsolvedSegmentKind::Circle {
1852            center,
1853            start,
1854            center_object_id,
1855            start_object_id,
1856            ..
1857        } => {
1858            // Extract center and start point coordinates
1859            match (&center[0], &center[1], &start[0], &start[1]) {
1860                (
1861                    UnsolvedExpr::Unknown(center_x),
1862                    UnsolvedExpr::Unknown(center_y),
1863                    UnsolvedExpr::Unknown(start_x),
1864                    UnsolvedExpr::Unknown(start_y),
1865                ) => {
1866                    // All coordinates are sketch vars. Create constraint.
1867                    let sketch_constraint = SketchConstraint {
1868                        kind: constraint_kind([
1869                            ConstrainablePoint2d {
1870                                vars: crate::front::Point2d {
1871                                    x: *center_x,
1872                                    y: *center_y,
1873                                },
1874                                object_id: *center_object_id,
1875                            },
1876                            ConstrainablePoint2d {
1877                                vars: crate::front::Point2d {
1878                                    x: *start_x,
1879                                    y: *start_y,
1880                                },
1881                                object_id: *start_object_id,
1882                            },
1883                        ]),
1884                        meta: vec![source_range.into()],
1885                    };
1886                    Ok(sketch_constraint)
1887                }
1888                _ => Err(KclError::new_semantic(KclErrorDetails::new(
1889                    format!(
1890                        "unimplemented: {}() arc or circle segment must have all sketch vars in all coordinates",
1891                        function_name
1892                    ),
1893                    vec![source_range],
1894                ))),
1895            }
1896        }
1897        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1898            format!("{}() argument must be an arc or circle segment", function_name),
1899            vec![source_range],
1900        ))),
1901    }
1902}
1903
1904pub async fn radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1905    let segment: KclValue =
1906        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
1907
1908    create_circular_radius_constraint(
1909        segment,
1910        |points| SketchConstraintKind::Radius { points },
1911        args.source_range,
1912    )
1913    .map(|constraint| KclValue::SketchConstraint {
1914        value: Box::new(constraint),
1915    })
1916}
1917
1918pub async fn diameter(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1919    let segment: KclValue =
1920        args.get_unlabeled_kw_arg("points", &RuntimeType::Primitive(PrimitiveType::Any), exec_state)?;
1921
1922    create_circular_radius_constraint(
1923        segment,
1924        |points| SketchConstraintKind::Diameter { points },
1925        args.source_range,
1926    )
1927    .map(|constraint| KclValue::SketchConstraint {
1928        value: Box::new(constraint),
1929    })
1930}
1931
1932pub async fn horizontal_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1933    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
1934        "points",
1935        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
1936        exec_state,
1937    )?;
1938    let [p1, p2] = points.as_slice() else {
1939        return Err(KclError::new_semantic(KclErrorDetails::new(
1940            "must have two input points".to_owned(),
1941            vec![args.source_range],
1942        )));
1943    };
1944    match (p1, p2) {
1945        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
1946            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
1947                return Err(KclError::new_semantic(KclErrorDetails::new(
1948                    "first point must be an unsolved segment".to_owned(),
1949                    vec![args.source_range],
1950                )));
1951            };
1952            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
1953                return Err(KclError::new_semantic(KclErrorDetails::new(
1954                    "second point must be an unsolved segment".to_owned(),
1955                    vec![args.source_range],
1956                )));
1957            };
1958            match (&unsolved0.kind, &unsolved1.kind) {
1959                (
1960                    UnsolvedSegmentKind::Point { position: pos0, .. },
1961                    UnsolvedSegmentKind::Point { position: pos1, .. },
1962                ) => {
1963                    // Both segments are points. Create a horizontal distance constraint
1964                    // between them.
1965                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
1966                        (
1967                            UnsolvedExpr::Unknown(p0_x),
1968                            UnsolvedExpr::Unknown(p0_y),
1969                            UnsolvedExpr::Unknown(p1_x),
1970                            UnsolvedExpr::Unknown(p1_y),
1971                        ) => {
1972                            // All coordinates are sketch vars. Proceed.
1973                            let sketch_constraint = SketchConstraint {
1974                                kind: SketchConstraintKind::HorizontalDistance {
1975                                    points: [
1976                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1977                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
1978                                            object_id: unsolved0.object_id,
1979                                        }),
1980                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
1981                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
1982                                            object_id: unsolved1.object_id,
1983                                        }),
1984                                    ],
1985                                },
1986                                meta: vec![args.source_range.into()],
1987                            };
1988                            Ok(KclValue::SketchConstraint {
1989                                value: Box::new(sketch_constraint),
1990                            })
1991                        }
1992                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
1993                            "unimplemented: horizontalDistance() arguments must be all sketch vars in all coordinates"
1994                                .to_owned(),
1995                            vec![args.source_range],
1996                        ))),
1997                    }
1998                }
1999                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2000                    "horizontalDistance() arguments must be unsolved points".to_owned(),
2001                    vec![args.source_range],
2002                ))),
2003            }
2004        }
2005        // Segment + point-literal branch; for now the only supported Point2d literal here is ORIGIN.
2006        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2007            if !point2d_is_origin(point2d) {
2008                return Err(KclError::new_semantic(KclErrorDetails::new(
2009                    "horizontalDistance() Point2d arguments must be ORIGIN".to_owned(),
2010                    vec![args.source_range],
2011                )));
2012            }
2013
2014            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2015                return Err(KclError::new_semantic(KclErrorDetails::new(
2016                    "segment must be an unsolved segment".to_owned(),
2017                    vec![args.source_range],
2018                )));
2019            };
2020            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2021                return Err(KclError::new_semantic(KclErrorDetails::new(
2022                    "horizontalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2023                    vec![args.source_range],
2024                )));
2025            };
2026            match (&position[0], &position[1]) {
2027                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2028                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2029                        vars: crate::front::Point2d {
2030                            x: *point_x,
2031                            y: *point_y,
2032                        },
2033                        object_id: unsolved.object_id,
2034                    });
2035                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2036                        [point, ConstrainablePoint2dOrOrigin::Origin]
2037                    } else {
2038                        [ConstrainablePoint2dOrOrigin::Origin, point]
2039                    };
2040                    Ok(KclValue::SketchConstraint {
2041                        value: Box::new(SketchConstraint {
2042                            kind: SketchConstraintKind::HorizontalDistance { points },
2043                            meta: vec![args.source_range.into()],
2044                        }),
2045                    })
2046                }
2047                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2048                    "unimplemented: horizontalDistance() point arguments must be sketch vars in all coordinates"
2049                        .to_owned(),
2050                    vec![args.source_range],
2051                ))),
2052            }
2053        }
2054        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2055            "horizontalDistance() arguments must be point segments or ORIGIN".to_owned(),
2056            vec![args.source_range],
2057        ))),
2058    }
2059}
2060
2061pub async fn vertical_distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2062    let points: Vec<KclValue> = args.get_unlabeled_kw_arg(
2063        "points",
2064        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2065        exec_state,
2066    )?;
2067    let [p1, p2] = points.as_slice() else {
2068        return Err(KclError::new_semantic(KclErrorDetails::new(
2069            "must have two input points".to_owned(),
2070            vec![args.source_range],
2071        )));
2072    };
2073    match (p1, p2) {
2074        (KclValue::Segment { value: seg0 }, KclValue::Segment { value: seg1 }) => {
2075            let SegmentRepr::Unsolved { segment: unsolved0 } = &seg0.repr else {
2076                return Err(KclError::new_semantic(KclErrorDetails::new(
2077                    "first point must be an unsolved segment".to_owned(),
2078                    vec![args.source_range],
2079                )));
2080            };
2081            let SegmentRepr::Unsolved { segment: unsolved1 } = &seg1.repr else {
2082                return Err(KclError::new_semantic(KclErrorDetails::new(
2083                    "second point must be an unsolved segment".to_owned(),
2084                    vec![args.source_range],
2085                )));
2086            };
2087            match (&unsolved0.kind, &unsolved1.kind) {
2088                (
2089                    UnsolvedSegmentKind::Point { position: pos0, .. },
2090                    UnsolvedSegmentKind::Point { position: pos1, .. },
2091                ) => {
2092                    // Both segments are points. Create a vertical distance constraint
2093                    // between them.
2094                    match (&pos0[0], &pos0[1], &pos1[0], &pos1[1]) {
2095                        (
2096                            UnsolvedExpr::Unknown(p0_x),
2097                            UnsolvedExpr::Unknown(p0_y),
2098                            UnsolvedExpr::Unknown(p1_x),
2099                            UnsolvedExpr::Unknown(p1_y),
2100                        ) => {
2101                            // All coordinates are sketch vars. Proceed.
2102                            let sketch_constraint = SketchConstraint {
2103                                kind: SketchConstraintKind::VerticalDistance {
2104                                    points: [
2105                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2106                                            vars: crate::front::Point2d { x: *p0_x, y: *p0_y },
2107                                            object_id: unsolved0.object_id,
2108                                        }),
2109                                        ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2110                                            vars: crate::front::Point2d { x: *p1_x, y: *p1_y },
2111                                            object_id: unsolved1.object_id,
2112                                        }),
2113                                    ],
2114                                },
2115                                meta: vec![args.source_range.into()],
2116                            };
2117                            Ok(KclValue::SketchConstraint {
2118                                value: Box::new(sketch_constraint),
2119                            })
2120                        }
2121                        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2122                            "unimplemented: verticalDistance() arguments must be all sketch vars in all coordinates"
2123                                .to_owned(),
2124                            vec![args.source_range],
2125                        ))),
2126                    }
2127                }
2128                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2129                    "verticalDistance() arguments must be unsolved points".to_owned(),
2130                    vec![args.source_range],
2131                ))),
2132            }
2133        }
2134        (KclValue::Segment { value: seg }, point2d) | (point2d, KclValue::Segment { value: seg }) => {
2135            if !point2d_is_origin(point2d) {
2136                return Err(KclError::new_semantic(KclErrorDetails::new(
2137                    "verticalDistance() Point2d arguments must be ORIGIN".to_owned(),
2138                    vec![args.source_range],
2139                )));
2140            }
2141
2142            let SegmentRepr::Unsolved { segment: unsolved } = &seg.repr else {
2143                return Err(KclError::new_semantic(KclErrorDetails::new(
2144                    "segment must be an unsolved segment".to_owned(),
2145                    vec![args.source_range],
2146                )));
2147            };
2148            let UnsolvedSegmentKind::Point { position, .. } = &unsolved.kind else {
2149                return Err(KclError::new_semantic(KclErrorDetails::new(
2150                    "verticalDistance() arguments must be unsolved points or ORIGIN".to_owned(),
2151                    vec![args.source_range],
2152                )));
2153            };
2154            match (&position[0], &position[1]) {
2155                (UnsolvedExpr::Unknown(point_x), UnsolvedExpr::Unknown(point_y)) => {
2156                    let point = ConstrainablePoint2dOrOrigin::Point(ConstrainablePoint2d {
2157                        vars: crate::front::Point2d {
2158                            x: *point_x,
2159                            y: *point_y,
2160                        },
2161                        object_id: unsolved.object_id,
2162                    });
2163                    let points = if matches!((p1, p2), (KclValue::Segment { .. }, _)) {
2164                        [point, ConstrainablePoint2dOrOrigin::Origin]
2165                    } else {
2166                        [ConstrainablePoint2dOrOrigin::Origin, point]
2167                    };
2168                    Ok(KclValue::SketchConstraint {
2169                        value: Box::new(SketchConstraint {
2170                            kind: SketchConstraintKind::VerticalDistance { points },
2171                            meta: vec![args.source_range.into()],
2172                        }),
2173                    })
2174                }
2175                _ => Err(KclError::new_semantic(KclErrorDetails::new(
2176                    "unimplemented: verticalDistance() point arguments must be sketch vars in all coordinates"
2177                        .to_owned(),
2178                    vec![args.source_range],
2179                ))),
2180            }
2181        }
2182        _ => Err(KclError::new_semantic(KclErrorDetails::new(
2183            "verticalDistance() arguments must be point segments or ORIGIN".to_owned(),
2184            vec![args.source_range],
2185        ))),
2186    }
2187}
2188
2189pub async fn equal_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2190    #[derive(Clone, Copy)]
2191    struct ConstrainableLine {
2192        solver_line: DatumLineSegment,
2193        #[cfg(feature = "artifact-graph")]
2194        object_id: ObjectId,
2195    }
2196
2197    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2198        "lines",
2199        &RuntimeType::Array(
2200            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2201            ArrayLen::Minimum(2),
2202        ),
2203        exec_state,
2204    )?;
2205    let range = args.source_range;
2206    let constrainable_lines: Vec<ConstrainableLine> = lines
2207        .iter()
2208        .map(|line| {
2209            let KclValue::Segment { value: segment } = line else {
2210                return Err(KclError::new_semantic(KclErrorDetails::new(
2211                    "line argument must be a Segment".to_owned(),
2212                    vec![args.source_range],
2213                )));
2214            };
2215            let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2216                return Err(KclError::new_internal(KclErrorDetails::new(
2217                    "line must be an unsolved Segment".to_owned(),
2218                    vec![args.source_range],
2219                )));
2220            };
2221            let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
2222                return Err(KclError::new_semantic(KclErrorDetails::new(
2223                    "line argument must be a line, no other type of Segment".to_owned(),
2224                    vec![args.source_range],
2225                )));
2226            };
2227            let UnsolvedExpr::Unknown(line_p0_x) = &start[0] else {
2228                return Err(KclError::new_semantic(KclErrorDetails::new(
2229                    "line's start x coordinate must be a var".to_owned(),
2230                    vec![args.source_range],
2231                )));
2232            };
2233            let UnsolvedExpr::Unknown(line_p0_y) = &start[1] else {
2234                return Err(KclError::new_semantic(KclErrorDetails::new(
2235                    "line's start y coordinate must be a var".to_owned(),
2236                    vec![args.source_range],
2237                )));
2238            };
2239            let UnsolvedExpr::Unknown(line_p1_x) = &end[0] else {
2240                return Err(KclError::new_semantic(KclErrorDetails::new(
2241                    "line's end x coordinate must be a var".to_owned(),
2242                    vec![args.source_range],
2243                )));
2244            };
2245            let UnsolvedExpr::Unknown(line_p1_y) = &end[1] else {
2246                return Err(KclError::new_semantic(KclErrorDetails::new(
2247                    "line's end y coordinate must be a var".to_owned(),
2248                    vec![args.source_range],
2249                )));
2250            };
2251
2252            let solver_line_p0 =
2253                DatumPoint::new_xy(line_p0_x.to_constraint_id(range)?, line_p0_y.to_constraint_id(range)?);
2254            let solver_line_p1 =
2255                DatumPoint::new_xy(line_p1_x.to_constraint_id(range)?, line_p1_y.to_constraint_id(range)?);
2256
2257            Ok(ConstrainableLine {
2258                solver_line: DatumLineSegment::new(solver_line_p0, solver_line_p1),
2259                #[cfg(feature = "artifact-graph")]
2260                object_id: unsolved.object_id,
2261            })
2262        })
2263        .collect::<Result<_, _>>()?;
2264
2265    #[cfg(feature = "artifact-graph")]
2266    let constraint_id = exec_state.next_object_id();
2267    // Save the constraint to be used for solving.
2268    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2269        return Err(KclError::new_semantic(KclErrorDetails::new(
2270            "equalLength() can only be used inside a sketch block".to_owned(),
2271            vec![args.source_range],
2272        )));
2273    };
2274    let first_line = constrainable_lines[0];
2275    for line in constrainable_lines.iter().skip(1) {
2276        sketch_state.solver_constraints.push(SolverConstraint::LinesEqualLength(
2277            first_line.solver_line,
2278            line.solver_line,
2279        ));
2280    }
2281    #[cfg(feature = "artifact-graph")]
2282    {
2283        let constraint = crate::front::Constraint::LinesEqualLength(LinesEqualLength {
2284            lines: constrainable_lines.iter().map(|line| line.object_id).collect(),
2285        });
2286        sketch_state.sketch_constraints.push(constraint_id);
2287        track_constraint(constraint_id, constraint, exec_state, &args);
2288    }
2289    Ok(KclValue::none())
2290}
2291
2292fn datum_point(coords: [SketchVarId; 2], range: crate::SourceRange) -> Result<DatumPoint, KclError> {
2293    Ok(DatumPoint::new_xy(
2294        coords[0].to_constraint_id(range)?,
2295        coords[1].to_constraint_id(range)?,
2296    ))
2297}
2298
2299fn sketch_var_initial_value(
2300    sketch_vars: &[KclValue],
2301    id: SketchVarId,
2302    exec_state: &mut ExecState,
2303    range: crate::SourceRange,
2304) -> Result<f64, KclError> {
2305    sketch_vars
2306        .get(id.0)
2307        .and_then(KclValue::as_sketch_var)
2308        .map(|sketch_var| {
2309            sketch_var
2310                .initial_value_to_solver_units(exec_state, range, "equalRadius() hidden shared radius initial value")
2311                .map(|value| value.n)
2312        })
2313        .transpose()?
2314        .ok_or_else(|| {
2315            KclError::new_internal(KclErrorDetails::new(
2316                format!("Missing sketch variable initial value for id {}", id.0),
2317                vec![range],
2318            ))
2319        })
2320}
2321
2322fn radius_guess(
2323    sketch_vars: &[KclValue],
2324    center: [SketchVarId; 2],
2325    point: [SketchVarId; 2],
2326    exec_state: &mut ExecState,
2327    range: crate::SourceRange,
2328) -> Result<f64, KclError> {
2329    let dx = sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?
2330        - sketch_var_initial_value(sketch_vars, center[0], exec_state, range)?;
2331    let dy = sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?
2332        - sketch_var_initial_value(sketch_vars, center[1], exec_state, range)?;
2333    Ok(libm::hypot(dx, dy))
2334}
2335
2336pub async fn equal_radius(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2337    #[derive(Debug, Clone, Copy)]
2338    struct RadiusInputVars {
2339        center: [SketchVarId; 2],
2340        start: [SketchVarId; 2],
2341        end: Option<[SketchVarId; 2]>,
2342    }
2343
2344    #[derive(Debug, Clone, Copy)]
2345    enum EqualRadiusInput {
2346        Radius(RadiusInputVars),
2347    }
2348
2349    fn extract_equal_radius_input(
2350        segment_value: &KclValue,
2351        range: crate::SourceRange,
2352    ) -> Result<(EqualRadiusInput, ObjectId), KclError> {
2353        let KclValue::Segment { value: segment } = segment_value else {
2354            return Err(KclError::new_semantic(KclErrorDetails::new(
2355                format!(
2356                    "equalRadius() arguments must be segments but found {}",
2357                    segment_value.human_friendly_type()
2358                ),
2359                vec![range],
2360            )));
2361        };
2362        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2363            return Err(KclError::new_semantic(KclErrorDetails::new(
2364                "equalRadius() arguments must be unsolved segments".to_owned(),
2365                vec![range],
2366            )));
2367        };
2368        match &unsolved.kind {
2369            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2370                let (
2371                    UnsolvedExpr::Unknown(center_x),
2372                    UnsolvedExpr::Unknown(center_y),
2373                    UnsolvedExpr::Unknown(start_x),
2374                    UnsolvedExpr::Unknown(start_y),
2375                    UnsolvedExpr::Unknown(end_x),
2376                    UnsolvedExpr::Unknown(end_y),
2377                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2378                else {
2379                    return Err(KclError::new_semantic(KclErrorDetails::new(
2380                        "arc center/start/end coordinates must be sketch vars for equalRadius()".to_owned(),
2381                        vec![range],
2382                    )));
2383                };
2384                Ok((
2385                    EqualRadiusInput::Radius(RadiusInputVars {
2386                        center: [*center_x, *center_y],
2387                        start: [*start_x, *start_y],
2388                        end: Some([*end_x, *end_y]),
2389                    }),
2390                    unsolved.object_id,
2391                ))
2392            }
2393            UnsolvedSegmentKind::Circle { center, start, .. } => {
2394                let (
2395                    UnsolvedExpr::Unknown(center_x),
2396                    UnsolvedExpr::Unknown(center_y),
2397                    UnsolvedExpr::Unknown(start_x),
2398                    UnsolvedExpr::Unknown(start_y),
2399                ) = (&center[0], &center[1], &start[0], &start[1])
2400                else {
2401                    return Err(KclError::new_semantic(KclErrorDetails::new(
2402                        "circle center/start coordinates must be sketch vars for equalRadius()".to_owned(),
2403                        vec![range],
2404                    )));
2405                };
2406                Ok((
2407                    EqualRadiusInput::Radius(RadiusInputVars {
2408                        center: [*center_x, *center_y],
2409                        start: [*start_x, *start_y],
2410                        end: None,
2411                    }),
2412                    unsolved.object_id,
2413                ))
2414            }
2415            other => Err(KclError::new_semantic(KclErrorDetails::new(
2416                format!(
2417                    "equalRadius() currently supports only arc and circle segments, you provided {}",
2418                    other.human_friendly_kind_with_article()
2419                ),
2420                vec![range],
2421            ))),
2422        }
2423    }
2424
2425    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2426        "input",
2427        &RuntimeType::Array(
2428            Box::new(RuntimeType::Primitive(PrimitiveType::Any)),
2429            ArrayLen::Minimum(2),
2430        ),
2431        exec_state,
2432    )?;
2433    let range = args.source_range;
2434
2435    let extracted_input = input
2436        .iter()
2437        .map(|segment_value| extract_equal_radius_input(segment_value, range))
2438        .collect::<Result<Vec<_>, _>>()?;
2439    let radius_inputs: Vec<RadiusInputVars> = extracted_input
2440        .iter()
2441        .map(|(equal_radius_input, _)| match equal_radius_input {
2442            EqualRadiusInput::Radius(radius_input) => *radius_input,
2443        })
2444        .collect();
2445    #[cfg(feature = "artifact-graph")]
2446    let input_object_ids: Vec<ObjectId> = extracted_input.iter().map(|(_, object_id)| *object_id).collect();
2447
2448    let sketch_var_ty = solver_numeric_type(exec_state);
2449    #[cfg(feature = "artifact-graph")]
2450    let constraint_id = exec_state.next_object_id();
2451
2452    let sketch_vars = {
2453        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2454            return Err(KclError::new_semantic(KclErrorDetails::new(
2455                "equalRadius() can only be used inside a sketch block".to_owned(),
2456                vec![range],
2457            )));
2458        };
2459        sketch_state.sketch_vars.clone()
2460    };
2461
2462    let radius_initial_value = radius_guess(
2463        &sketch_vars,
2464        radius_inputs[0].center,
2465        radius_inputs[0].start,
2466        exec_state,
2467        range,
2468    )?;
2469
2470    let Some(sketch_state) = exec_state.sketch_block_mut() else {
2471        return Err(KclError::new_semantic(KclErrorDetails::new(
2472            "equalRadius() can only be used inside a sketch block".to_owned(),
2473            vec![range],
2474        )));
2475    };
2476    let radius_id = sketch_state.next_sketch_var_id();
2477    sketch_state.sketch_vars.push(KclValue::SketchVar {
2478        value: Box::new(crate::execution::SketchVar {
2479            id: radius_id,
2480            initial_value: radius_initial_value,
2481            ty: sketch_var_ty,
2482            meta: vec![],
2483        }),
2484    });
2485    let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2486
2487    for radius_input in radius_inputs {
2488        let center = datum_point(radius_input.center, range)?;
2489        let start = datum_point(radius_input.start, range)?;
2490        sketch_state
2491            .solver_constraints
2492            .push(SolverConstraint::DistanceVar(start, center, radius));
2493        if let Some(end) = radius_input.end {
2494            let end = datum_point(end, range)?;
2495            sketch_state
2496                .solver_constraints
2497                .push(SolverConstraint::DistanceVar(end, center, radius));
2498        }
2499    }
2500
2501    #[cfg(feature = "artifact-graph")]
2502    {
2503        let constraint = crate::front::Constraint::EqualRadius(EqualRadius {
2504            input: input_object_ids,
2505        });
2506        sketch_state.sketch_constraints.push(constraint_id);
2507        track_constraint(constraint_id, constraint, exec_state, &args);
2508    }
2509
2510    Ok(KclValue::none())
2511}
2512
2513pub async fn tangent(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2514    #[derive(Debug, Clone, Copy)]
2515    struct ConstrainableLineVars {
2516        start: [SketchVarId; 2],
2517        end: [SketchVarId; 2],
2518    }
2519
2520    #[derive(Debug, Clone, Copy)]
2521    struct ConstrainableCircularVars {
2522        center: [SketchVarId; 2],
2523        start: [SketchVarId; 2],
2524        end: Option<[SketchVarId; 2]>,
2525    }
2526
2527    #[derive(Debug, Clone, Copy)]
2528    enum TangentInput {
2529        Line(ConstrainableLineVars),
2530        Circular(ConstrainableCircularVars),
2531    }
2532
2533    fn extract_tangent_input(
2534        segment_value: &KclValue,
2535        range: crate::SourceRange,
2536    ) -> Result<(TangentInput, ObjectId), KclError> {
2537        let KclValue::Segment { value: segment } = segment_value else {
2538            return Err(KclError::new_semantic(KclErrorDetails::new(
2539                "tangent() arguments must be segments".to_owned(),
2540                vec![range],
2541            )));
2542        };
2543        let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
2544            return Err(KclError::new_semantic(KclErrorDetails::new(
2545                "tangent() arguments must be unsolved segments".to_owned(),
2546                vec![range],
2547            )));
2548        };
2549        match &unsolved.kind {
2550            UnsolvedSegmentKind::Line { start, end, .. } => {
2551                let (
2552                    UnsolvedExpr::Unknown(start_x),
2553                    UnsolvedExpr::Unknown(start_y),
2554                    UnsolvedExpr::Unknown(end_x),
2555                    UnsolvedExpr::Unknown(end_y),
2556                ) = (&start[0], &start[1], &end[0], &end[1])
2557                else {
2558                    return Err(KclError::new_semantic(KclErrorDetails::new(
2559                        "line coordinates must be sketch vars for tangent()".to_owned(),
2560                        vec![range],
2561                    )));
2562                };
2563                Ok((
2564                    TangentInput::Line(ConstrainableLineVars {
2565                        start: [*start_x, *start_y],
2566                        end: [*end_x, *end_y],
2567                    }),
2568                    unsolved.object_id,
2569                ))
2570            }
2571            UnsolvedSegmentKind::Arc { center, start, end, .. } => {
2572                let (
2573                    UnsolvedExpr::Unknown(center_x),
2574                    UnsolvedExpr::Unknown(center_y),
2575                    UnsolvedExpr::Unknown(start_x),
2576                    UnsolvedExpr::Unknown(start_y),
2577                    UnsolvedExpr::Unknown(end_x),
2578                    UnsolvedExpr::Unknown(end_y),
2579                ) = (&center[0], &center[1], &start[0], &start[1], &end[0], &end[1])
2580                else {
2581                    return Err(KclError::new_semantic(KclErrorDetails::new(
2582                        "arc center/start/end coordinates must be sketch vars for tangent()".to_owned(),
2583                        vec![range],
2584                    )));
2585                };
2586                Ok((
2587                    TangentInput::Circular(ConstrainableCircularVars {
2588                        center: [*center_x, *center_y],
2589                        start: [*start_x, *start_y],
2590                        end: Some([*end_x, *end_y]),
2591                    }),
2592                    unsolved.object_id,
2593                ))
2594            }
2595            UnsolvedSegmentKind::Circle { center, start, .. } => {
2596                let (
2597                    UnsolvedExpr::Unknown(center_x),
2598                    UnsolvedExpr::Unknown(center_y),
2599                    UnsolvedExpr::Unknown(start_x),
2600                    UnsolvedExpr::Unknown(start_y),
2601                ) = (&center[0], &center[1], &start[0], &start[1])
2602                else {
2603                    return Err(KclError::new_semantic(KclErrorDetails::new(
2604                        "circle center/start coordinates must be sketch vars for tangent()".to_owned(),
2605                        vec![range],
2606                    )));
2607                };
2608                Ok((
2609                    TangentInput::Circular(ConstrainableCircularVars {
2610                        center: [*center_x, *center_y],
2611                        start: [*start_x, *start_y],
2612                        end: None,
2613                    }),
2614                    unsolved.object_id,
2615                ))
2616            }
2617            _ => Err(KclError::new_semantic(KclErrorDetails::new(
2618                "tangent() supports only line, arc, and circle segments".to_owned(),
2619                vec![range],
2620            ))),
2621        }
2622    }
2623
2624    fn point_initial_position(
2625        sketch_vars: &[KclValue],
2626        point: [SketchVarId; 2],
2627        exec_state: &mut ExecState,
2628        range: crate::SourceRange,
2629    ) -> Result<[f64; 2], KclError> {
2630        Ok([
2631            sketch_var_initial_value(sketch_vars, point[0], exec_state, range)?,
2632            sketch_var_initial_value(sketch_vars, point[1], exec_state, range)?,
2633        ])
2634    }
2635
2636    fn canonicalize_line_for_tangent(
2637        sketch_vars: &[KclValue],
2638        line: ConstrainableLineVars,
2639        arc_center: [SketchVarId; 2],
2640        exec_state: &mut ExecState,
2641        range: crate::SourceRange,
2642    ) -> Result<ConstrainableLineVars, KclError> {
2643        let [sx, sy] = point_initial_position(sketch_vars, line.start, exec_state, range)?;
2644        let [ex, ey] = point_initial_position(sketch_vars, line.end, exec_state, range)?;
2645        let [cx, cy] = point_initial_position(sketch_vars, arc_center, exec_state, range)?;
2646
2647        // Canonicalize the line orientation so LineTangentToCircle sees the arc
2648        // center on its non-negative side regardless of how the user ordered the
2649        // line endpoints in KCL.
2650        let signed_side = (ex - sx) * (cy - sy) - (ey - sy) * (cx - sx);
2651        if signed_side < -1e-9 {
2652            Ok(ConstrainableLineVars {
2653                start: line.end,
2654                end: line.start,
2655            })
2656        } else {
2657            Ok(line)
2658        }
2659    }
2660
2661    let input: Vec<KclValue> = args.get_unlabeled_kw_arg(
2662        "input",
2663        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2664        exec_state,
2665    )?;
2666    let [item0, item1]: [KclValue; 2] = input.try_into().map_err(|_| {
2667        KclError::new_semantic(KclErrorDetails::new(
2668            "tangent() requires exactly 2 input segments".to_owned(),
2669            vec![args.source_range],
2670        ))
2671    })?;
2672    let range = args.source_range;
2673    let (input0, input0_object_id) = extract_tangent_input(&item0, range)?;
2674    let (input1, input1_object_id) = extract_tangent_input(&item1, range)?;
2675    #[cfg(not(feature = "artifact-graph"))]
2676    let _ = (input0_object_id, input1_object_id);
2677
2678    enum TangentCase {
2679        LineCircular(ConstrainableLineVars, ConstrainableCircularVars),
2680        CircularCircular(ConstrainableCircularVars, ConstrainableCircularVars),
2681    }
2682    let tangent_case = match (input0, input1) {
2683        (TangentInput::Line(line), TangentInput::Circular(circular))
2684        | (TangentInput::Circular(circular), TangentInput::Line(line)) => TangentCase::LineCircular(line, circular),
2685        (TangentInput::Circular(circular0), TangentInput::Circular(circular1)) => {
2686            TangentCase::CircularCircular(circular0, circular1)
2687        }
2688        (TangentInput::Line(_), TangentInput::Line(_)) => {
2689            return Err(KclError::new_semantic(KclErrorDetails::new(
2690                "tangent() does not support Line/Line. Tangency requires at least one circular segment.".to_owned(),
2691                vec![range],
2692            )));
2693        }
2694    };
2695
2696    let sketch_var_ty = solver_numeric_type(exec_state);
2697    #[cfg(feature = "artifact-graph")]
2698    let constraint_id = exec_state.next_object_id();
2699
2700    let sketch_vars = {
2701        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2702            return Err(KclError::new_semantic(KclErrorDetails::new(
2703                "tangent() can only be used inside a sketch block".to_owned(),
2704                vec![range],
2705            )));
2706        };
2707        sketch_state.sketch_vars.clone()
2708    };
2709
2710    // Hidden radius vars. Empty metadata keeps them out of source write-back.
2711    match tangent_case {
2712        TangentCase::LineCircular(line, circular) => {
2713            let canonical_line = canonicalize_line_for_tangent(&sketch_vars, line, circular.center, exec_state, range)?;
2714            let line_p0 = datum_point(canonical_line.start, range)?;
2715            let line_p1 = datum_point(canonical_line.end, range)?;
2716            let line_datum = DatumLineSegment::new(line_p0, line_p1);
2717
2718            let center = datum_point(circular.center, range)?;
2719            let circular_start = datum_point(circular.start, range)?;
2720            let circular_end = circular.end.map(|end| datum_point(end, range)).transpose()?;
2721            let radius_initial_value = radius_guess(&sketch_vars, circular.center, circular.start, exec_state, range)?;
2722            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2723                return Err(KclError::new_semantic(KclErrorDetails::new(
2724                    "tangent() can only be used inside a sketch block".to_owned(),
2725                    vec![range],
2726                )));
2727            };
2728            let radius_id = sketch_state.next_sketch_var_id();
2729            sketch_state.sketch_vars.push(KclValue::SketchVar {
2730                value: Box::new(crate::execution::SketchVar {
2731                    id: radius_id,
2732                    initial_value: radius_initial_value,
2733                    ty: sketch_var_ty,
2734                    meta: vec![],
2735                }),
2736            });
2737            let radius = DatumDistance::new(radius_id.to_constraint_id(range)?);
2738            let circle = DatumCircle { center, radius };
2739
2740            // Tangency decomposition for Line/circular segment:
2741            // 1) Introduce a hidden radius variable r for the segment's underlying circle.
2742            // 2) Keep the segment's defining points on that circle with DistanceVar(point, center, r).
2743            // 3) Canonicalize the solver line orientation so endpoint order
2744            //    doesn't change the tangent branch.
2745            // 4) Apply the native LineTangentToCircle solver constraint.
2746            sketch_state
2747                .solver_constraints
2748                .push(SolverConstraint::DistanceVar(circular_start, center, radius));
2749            if let Some(circular_end) = circular_end {
2750                sketch_state
2751                    .solver_constraints
2752                    .push(SolverConstraint::DistanceVar(circular_end, center, radius));
2753            }
2754            sketch_state
2755                .solver_constraints
2756                .push(SolverConstraint::LineTangentToCircle(line_datum, circle));
2757        }
2758        TangentCase::CircularCircular(circular0, circular1) => {
2759            let center0 = datum_point(circular0.center, range)?;
2760            let start0 = datum_point(circular0.start, range)?;
2761            let end0 = circular0.end.map(|end| datum_point(end, range)).transpose()?;
2762            let radius0_initial_value =
2763                radius_guess(&sketch_vars, circular0.center, circular0.start, exec_state, range)?;
2764            let center1 = datum_point(circular1.center, range)?;
2765            let start1 = datum_point(circular1.start, range)?;
2766            let end1 = circular1.end.map(|end| datum_point(end, range)).transpose()?;
2767            let radius1_initial_value =
2768                radius_guess(&sketch_vars, circular1.center, circular1.start, exec_state, range)?;
2769            let Some(sketch_state) = exec_state.sketch_block_mut() else {
2770                return Err(KclError::new_semantic(KclErrorDetails::new(
2771                    "tangent() can only be used inside a sketch block".to_owned(),
2772                    vec![range],
2773                )));
2774            };
2775            let radius0_id = sketch_state.next_sketch_var_id();
2776            sketch_state.sketch_vars.push(KclValue::SketchVar {
2777                value: Box::new(crate::execution::SketchVar {
2778                    id: radius0_id,
2779                    initial_value: radius0_initial_value,
2780                    ty: sketch_var_ty,
2781                    meta: vec![],
2782                }),
2783            });
2784            let radius0 = DatumDistance::new(radius0_id.to_constraint_id(range)?);
2785            let circle0 = DatumCircle {
2786                center: center0,
2787                radius: radius0,
2788            };
2789
2790            let radius1_id = sketch_state.next_sketch_var_id();
2791            sketch_state.sketch_vars.push(KclValue::SketchVar {
2792                value: Box::new(crate::execution::SketchVar {
2793                    id: radius1_id,
2794                    initial_value: radius1_initial_value,
2795                    ty: sketch_var_ty,
2796                    meta: vec![],
2797                }),
2798            });
2799            let radius1 = DatumDistance::new(radius1_id.to_constraint_id(range)?);
2800            let circle1 = DatumCircle {
2801                center: center1,
2802                radius: radius1,
2803            };
2804
2805            // Tangency decomposition for circular segment/circular segment:
2806            // 1) Introduce one hidden radius variable per arc.
2807            // 2) Keep each segment's defining points on its corresponding circle.
2808            // 3) Apply the native CircleTangentToCircle solver constraint.
2809            sketch_state
2810                .solver_constraints
2811                .push(SolverConstraint::DistanceVar(start0, center0, radius0));
2812            if let Some(end0) = end0 {
2813                sketch_state
2814                    .solver_constraints
2815                    .push(SolverConstraint::DistanceVar(end0, center0, radius0));
2816            }
2817            sketch_state
2818                .solver_constraints
2819                .push(SolverConstraint::DistanceVar(start1, center1, radius1));
2820            if let Some(end1) = end1 {
2821                sketch_state
2822                    .solver_constraints
2823                    .push(SolverConstraint::DistanceVar(end1, center1, radius1));
2824            }
2825            sketch_state
2826                .solver_constraints
2827                .push(SolverConstraint::CircleTangentToCircle(circle0, circle1));
2828        }
2829    }
2830
2831    #[cfg(feature = "artifact-graph")]
2832    {
2833        let constraint = crate::front::Constraint::Tangent(Tangent {
2834            input: vec![input0_object_id, input1_object_id],
2835        });
2836        let Some(sketch_state) = exec_state.sketch_block_mut() else {
2837            return Err(KclError::new_semantic(KclErrorDetails::new(
2838                "tangent() can only be used inside a sketch block".to_owned(),
2839                vec![range],
2840            )));
2841        };
2842        sketch_state.sketch_constraints.push(constraint_id);
2843        track_constraint(constraint_id, constraint, exec_state, &args);
2844    }
2845
2846    Ok(KclValue::none())
2847}
2848
2849#[derive(Debug, Clone, Copy)]
2850pub(crate) enum LinesAtAngleKind {
2851    Parallel,
2852    Perpendicular,
2853}
2854
2855impl LinesAtAngleKind {
2856    pub fn to_function_name(self) -> &'static str {
2857        match self {
2858            LinesAtAngleKind::Parallel => "parallel",
2859            LinesAtAngleKind::Perpendicular => "perpendicular",
2860        }
2861    }
2862
2863    fn to_solver_angle(self) -> ezpz::datatypes::AngleKind {
2864        match self {
2865            LinesAtAngleKind::Parallel => ezpz::datatypes::AngleKind::Parallel,
2866            LinesAtAngleKind::Perpendicular => ezpz::datatypes::AngleKind::Perpendicular,
2867        }
2868    }
2869
2870    #[cfg(feature = "artifact-graph")]
2871    fn constraint(&self, lines: Vec<ObjectId>) -> Constraint {
2872        match self {
2873            LinesAtAngleKind::Parallel => Constraint::Parallel(Parallel { lines }),
2874            LinesAtAngleKind::Perpendicular => Constraint::Perpendicular(Perpendicular { lines }),
2875        }
2876    }
2877}
2878
2879/// Convert between two different libraries with similar angle representations
2880#[expect(unused)]
2881fn into_kcmc_angle(angle: ezpz::datatypes::Angle) -> kcmc::shared::Angle {
2882    kcmc::shared::Angle::from_degrees(angle.to_degrees())
2883}
2884
2885/// Convert between two different libraries with similar angle representations
2886#[expect(unused)]
2887fn into_ezpz_angle(angle: kcmc::shared::Angle) -> ezpz::datatypes::Angle {
2888    ezpz::datatypes::Angle::from_degrees(angle.to_degrees())
2889}
2890
2891pub async fn parallel(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2892    lines_at_angle(LinesAtAngleKind::Parallel, exec_state, args).await
2893}
2894
2895pub async fn perpendicular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2896    lines_at_angle(LinesAtAngleKind::Perpendicular, exec_state, args).await
2897}
2898
2899pub async fn angle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2900    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
2901        "lines",
2902        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
2903        exec_state,
2904    )?;
2905    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
2906        KclError::new_semantic(KclErrorDetails::new(
2907            "must have two input lines".to_owned(),
2908            vec![args.source_range],
2909        ))
2910    })?;
2911    let KclValue::Segment { value: segment0 } = &line0 else {
2912        return Err(KclError::new_semantic(KclErrorDetails::new(
2913            "line argument must be a Segment".to_owned(),
2914            vec![args.source_range],
2915        )));
2916    };
2917    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
2918        return Err(KclError::new_internal(KclErrorDetails::new(
2919            "line must be an unsolved Segment".to_owned(),
2920            vec![args.source_range],
2921        )));
2922    };
2923    let UnsolvedSegmentKind::Line {
2924        start: start0,
2925        end: end0,
2926        ..
2927    } = &unsolved0.kind
2928    else {
2929        return Err(KclError::new_semantic(KclErrorDetails::new(
2930            "line argument must be a line, no other type of Segment".to_owned(),
2931            vec![args.source_range],
2932        )));
2933    };
2934    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
2935        return Err(KclError::new_semantic(KclErrorDetails::new(
2936            "line's start x coordinate must be a var".to_owned(),
2937            vec![args.source_range],
2938        )));
2939    };
2940    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
2941        return Err(KclError::new_semantic(KclErrorDetails::new(
2942            "line's start y coordinate must be a var".to_owned(),
2943            vec![args.source_range],
2944        )));
2945    };
2946    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
2947        return Err(KclError::new_semantic(KclErrorDetails::new(
2948            "line's end x coordinate must be a var".to_owned(),
2949            vec![args.source_range],
2950        )));
2951    };
2952    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
2953        return Err(KclError::new_semantic(KclErrorDetails::new(
2954            "line's end y coordinate must be a var".to_owned(),
2955            vec![args.source_range],
2956        )));
2957    };
2958    let KclValue::Segment { value: segment1 } = &line1 else {
2959        return Err(KclError::new_semantic(KclErrorDetails::new(
2960            "line argument must be a Segment".to_owned(),
2961            vec![args.source_range],
2962        )));
2963    };
2964    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
2965        return Err(KclError::new_internal(KclErrorDetails::new(
2966            "line must be an unsolved Segment".to_owned(),
2967            vec![args.source_range],
2968        )));
2969    };
2970    let UnsolvedSegmentKind::Line {
2971        start: start1,
2972        end: end1,
2973        ..
2974    } = &unsolved1.kind
2975    else {
2976        return Err(KclError::new_semantic(KclErrorDetails::new(
2977            "line argument must be a line, no other type of Segment".to_owned(),
2978            vec![args.source_range],
2979        )));
2980    };
2981    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
2982        return Err(KclError::new_semantic(KclErrorDetails::new(
2983            "line's start x coordinate must be a var".to_owned(),
2984            vec![args.source_range],
2985        )));
2986    };
2987    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
2988        return Err(KclError::new_semantic(KclErrorDetails::new(
2989            "line's start y coordinate must be a var".to_owned(),
2990            vec![args.source_range],
2991        )));
2992    };
2993    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
2994        return Err(KclError::new_semantic(KclErrorDetails::new(
2995            "line's end x coordinate must be a var".to_owned(),
2996            vec![args.source_range],
2997        )));
2998    };
2999    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3000        return Err(KclError::new_semantic(KclErrorDetails::new(
3001            "line's end y coordinate must be a var".to_owned(),
3002            vec![args.source_range],
3003        )));
3004    };
3005
3006    // All coordinates are sketch vars. Proceed.
3007    let sketch_constraint = SketchConstraint {
3008        kind: SketchConstraintKind::Angle {
3009            line0: crate::execution::ConstrainableLine2d {
3010                object_id: unsolved0.object_id,
3011                vars: [
3012                    crate::front::Point2d {
3013                        x: *line0_p0_x,
3014                        y: *line0_p0_y,
3015                    },
3016                    crate::front::Point2d {
3017                        x: *line0_p1_x,
3018                        y: *line0_p1_y,
3019                    },
3020                ],
3021            },
3022            line1: crate::execution::ConstrainableLine2d {
3023                object_id: unsolved1.object_id,
3024                vars: [
3025                    crate::front::Point2d {
3026                        x: *line1_p0_x,
3027                        y: *line1_p0_y,
3028                    },
3029                    crate::front::Point2d {
3030                        x: *line1_p1_x,
3031                        y: *line1_p1_y,
3032                    },
3033                ],
3034            },
3035        },
3036        meta: vec![args.source_range.into()],
3037    };
3038    Ok(KclValue::SketchConstraint {
3039        value: Box::new(sketch_constraint),
3040    })
3041}
3042
3043async fn lines_at_angle(
3044    angle_kind: LinesAtAngleKind,
3045    exec_state: &mut ExecState,
3046    args: Args,
3047) -> Result<KclValue, KclError> {
3048    let lines: Vec<KclValue> = args.get_unlabeled_kw_arg(
3049        "lines",
3050        &RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::Known(2)),
3051        exec_state,
3052    )?;
3053    let [line0, line1]: [KclValue; 2] = lines.try_into().map_err(|_| {
3054        KclError::new_semantic(KclErrorDetails::new(
3055            "must have two input lines".to_owned(),
3056            vec![args.source_range],
3057        ))
3058    })?;
3059
3060    let KclValue::Segment { value: segment0 } = &line0 else {
3061        return Err(KclError::new_semantic(KclErrorDetails::new(
3062            "line argument must be a Segment".to_owned(),
3063            vec![args.source_range],
3064        )));
3065    };
3066    let SegmentRepr::Unsolved { segment: unsolved0 } = &segment0.repr else {
3067        return Err(KclError::new_internal(KclErrorDetails::new(
3068            "line must be an unsolved Segment".to_owned(),
3069            vec![args.source_range],
3070        )));
3071    };
3072    let UnsolvedSegmentKind::Line {
3073        start: start0,
3074        end: end0,
3075        ..
3076    } = &unsolved0.kind
3077    else {
3078        return Err(KclError::new_semantic(KclErrorDetails::new(
3079            "line argument must be a line, no other type of Segment".to_owned(),
3080            vec![args.source_range],
3081        )));
3082    };
3083    let UnsolvedExpr::Unknown(line0_p0_x) = &start0[0] else {
3084        return Err(KclError::new_semantic(KclErrorDetails::new(
3085            "line's start x coordinate must be a var".to_owned(),
3086            vec![args.source_range],
3087        )));
3088    };
3089    let UnsolvedExpr::Unknown(line0_p0_y) = &start0[1] else {
3090        return Err(KclError::new_semantic(KclErrorDetails::new(
3091            "line's start y coordinate must be a var".to_owned(),
3092            vec![args.source_range],
3093        )));
3094    };
3095    let UnsolvedExpr::Unknown(line0_p1_x) = &end0[0] else {
3096        return Err(KclError::new_semantic(KclErrorDetails::new(
3097            "line's end x coordinate must be a var".to_owned(),
3098            vec![args.source_range],
3099        )));
3100    };
3101    let UnsolvedExpr::Unknown(line0_p1_y) = &end0[1] else {
3102        return Err(KclError::new_semantic(KclErrorDetails::new(
3103            "line's end y coordinate must be a var".to_owned(),
3104            vec![args.source_range],
3105        )));
3106    };
3107    let KclValue::Segment { value: segment1 } = &line1 else {
3108        return Err(KclError::new_semantic(KclErrorDetails::new(
3109            "line argument must be a Segment".to_owned(),
3110            vec![args.source_range],
3111        )));
3112    };
3113    let SegmentRepr::Unsolved { segment: unsolved1 } = &segment1.repr else {
3114        return Err(KclError::new_internal(KclErrorDetails::new(
3115            "line must be an unsolved Segment".to_owned(),
3116            vec![args.source_range],
3117        )));
3118    };
3119    let UnsolvedSegmentKind::Line {
3120        start: start1,
3121        end: end1,
3122        ..
3123    } = &unsolved1.kind
3124    else {
3125        return Err(KclError::new_semantic(KclErrorDetails::new(
3126            "line argument must be a line, no other type of Segment".to_owned(),
3127            vec![args.source_range],
3128        )));
3129    };
3130    let UnsolvedExpr::Unknown(line1_p0_x) = &start1[0] else {
3131        return Err(KclError::new_semantic(KclErrorDetails::new(
3132            "line's start x coordinate must be a var".to_owned(),
3133            vec![args.source_range],
3134        )));
3135    };
3136    let UnsolvedExpr::Unknown(line1_p0_y) = &start1[1] else {
3137        return Err(KclError::new_semantic(KclErrorDetails::new(
3138            "line's start y coordinate must be a var".to_owned(),
3139            vec![args.source_range],
3140        )));
3141    };
3142    let UnsolvedExpr::Unknown(line1_p1_x) = &end1[0] else {
3143        return Err(KclError::new_semantic(KclErrorDetails::new(
3144            "line's end x coordinate must be a var".to_owned(),
3145            vec![args.source_range],
3146        )));
3147    };
3148    let UnsolvedExpr::Unknown(line1_p1_y) = &end1[1] else {
3149        return Err(KclError::new_semantic(KclErrorDetails::new(
3150            "line's end y coordinate must be a var".to_owned(),
3151            vec![args.source_range],
3152        )));
3153    };
3154
3155    let range = args.source_range;
3156    let solver_line0_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3157        line0_p0_x.to_constraint_id(range)?,
3158        line0_p0_y.to_constraint_id(range)?,
3159    );
3160    let solver_line0_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3161        line0_p1_x.to_constraint_id(range)?,
3162        line0_p1_y.to_constraint_id(range)?,
3163    );
3164    let solver_line0 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line0_p0, solver_line0_p1);
3165    let solver_line1_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3166        line1_p0_x.to_constraint_id(range)?,
3167        line1_p0_y.to_constraint_id(range)?,
3168    );
3169    let solver_line1_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3170        line1_p1_x.to_constraint_id(range)?,
3171        line1_p1_y.to_constraint_id(range)?,
3172    );
3173    let solver_line1 = ezpz::datatypes::inputs::DatumLineSegment::new(solver_line1_p0, solver_line1_p1);
3174    let constraint = SolverConstraint::LinesAtAngle(solver_line0, solver_line1, angle_kind.to_solver_angle());
3175    #[cfg(feature = "artifact-graph")]
3176    let constraint_id = exec_state.next_object_id();
3177    // Save the constraint to be used for solving.
3178    let Some(sketch_state) = exec_state.sketch_block_mut() else {
3179        return Err(KclError::new_semantic(KclErrorDetails::new(
3180            format!(
3181                "{}() can only be used inside a sketch block",
3182                angle_kind.to_function_name()
3183            ),
3184            vec![args.source_range],
3185        )));
3186    };
3187    sketch_state.solver_constraints.push(constraint);
3188    #[cfg(feature = "artifact-graph")]
3189    {
3190        let constraint = angle_kind.constraint(vec![unsolved0.object_id, unsolved1.object_id]);
3191        sketch_state.sketch_constraints.push(constraint_id);
3192        track_constraint(constraint_id, constraint, exec_state, &args);
3193    }
3194    Ok(KclValue::none())
3195}
3196
3197pub async fn horizontal(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3198    let line: KclValue = args.get_unlabeled_kw_arg("line", &RuntimeType::segment(), exec_state)?;
3199    let KclValue::Segment { value: segment } = line else {
3200        return Err(KclError::new_semantic(KclErrorDetails::new(
3201            "line argument must be a Segment".to_owned(),
3202            vec![args.source_range],
3203        )));
3204    };
3205    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3206        return Err(KclError::new_internal(KclErrorDetails::new(
3207            "line must be an unsolved Segment".to_owned(),
3208            vec![args.source_range],
3209        )));
3210    };
3211    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3212        return Err(KclError::new_semantic(KclErrorDetails::new(
3213            "line argument must be a line, no other type of Segment".to_owned(),
3214            vec![args.source_range],
3215        )));
3216    };
3217    let p0_x = &start[0];
3218    let p0_y = &start[1];
3219    let p1_x = &end[0];
3220    let p1_y = &end[1];
3221    match (p0_x, p0_y, p1_x, p1_y) {
3222        (
3223            UnsolvedExpr::Unknown(p0_x),
3224            UnsolvedExpr::Unknown(p0_y),
3225            UnsolvedExpr::Unknown(p1_x),
3226            UnsolvedExpr::Unknown(p1_y),
3227        ) => {
3228            let range = args.source_range;
3229            let solver_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3230                p0_x.to_constraint_id(range)?,
3231                p0_y.to_constraint_id(range)?,
3232            );
3233            let solver_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3234                p1_x.to_constraint_id(range)?,
3235                p1_y.to_constraint_id(range)?,
3236            );
3237            let solver_line = ezpz::datatypes::inputs::DatumLineSegment::new(solver_p0, solver_p1);
3238            let constraint = ezpz::Constraint::Horizontal(solver_line);
3239            #[cfg(feature = "artifact-graph")]
3240            let constraint_id = exec_state.next_object_id();
3241            // Save the constraint to be used for solving.
3242            let Some(sketch_state) = exec_state.sketch_block_mut() else {
3243                return Err(KclError::new_semantic(KclErrorDetails::new(
3244                    "horizontal() can only be used inside a sketch block".to_owned(),
3245                    vec![args.source_range],
3246                )));
3247            };
3248            sketch_state.solver_constraints.push(constraint);
3249            #[cfg(feature = "artifact-graph")]
3250            {
3251                let constraint = crate::front::Constraint::Horizontal(Horizontal {
3252                    line: unsolved.object_id,
3253                });
3254                sketch_state.sketch_constraints.push(constraint_id);
3255                track_constraint(constraint_id, constraint, exec_state, &args);
3256            }
3257            Ok(KclValue::none())
3258        }
3259        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3260            "line's x and y coordinates of both start and end must be vars".to_owned(),
3261            vec![args.source_range],
3262        ))),
3263    }
3264}
3265
3266pub async fn vertical(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
3267    let line: KclValue = args.get_unlabeled_kw_arg("line", &RuntimeType::segment(), exec_state)?;
3268    let KclValue::Segment { value: segment } = line else {
3269        return Err(KclError::new_semantic(KclErrorDetails::new(
3270            "line argument must be a Segment".to_owned(),
3271            vec![args.source_range],
3272        )));
3273    };
3274    let SegmentRepr::Unsolved { segment: unsolved } = &segment.repr else {
3275        return Err(KclError::new_internal(KclErrorDetails::new(
3276            "line must be an unsolved Segment".to_owned(),
3277            vec![args.source_range],
3278        )));
3279    };
3280    let UnsolvedSegmentKind::Line { start, end, .. } = &unsolved.kind else {
3281        return Err(KclError::new_semantic(KclErrorDetails::new(
3282            "line argument must be a line, no other type of Segment".to_owned(),
3283            vec![args.source_range],
3284        )));
3285    };
3286    let p0_x = &start[0];
3287    let p0_y = &start[1];
3288    let p1_x = &end[0];
3289    let p1_y = &end[1];
3290    match (p0_x, p0_y, p1_x, p1_y) {
3291        (
3292            UnsolvedExpr::Unknown(p0_x),
3293            UnsolvedExpr::Unknown(p0_y),
3294            UnsolvedExpr::Unknown(p1_x),
3295            UnsolvedExpr::Unknown(p1_y),
3296        ) => {
3297            let range = args.source_range;
3298            let solver_p0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3299                p0_x.to_constraint_id(range)?,
3300                p0_y.to_constraint_id(range)?,
3301            );
3302            let solver_p1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
3303                p1_x.to_constraint_id(range)?,
3304                p1_y.to_constraint_id(range)?,
3305            );
3306            let solver_line = ezpz::datatypes::inputs::DatumLineSegment::new(solver_p0, solver_p1);
3307            let constraint = ezpz::Constraint::Vertical(solver_line);
3308            #[cfg(feature = "artifact-graph")]
3309            let constraint_id = exec_state.next_object_id();
3310            // Save the constraint to be used for solving.
3311            let Some(sketch_state) = exec_state.sketch_block_mut() else {
3312                return Err(KclError::new_semantic(KclErrorDetails::new(
3313                    "vertical() can only be used inside a sketch block".to_owned(),
3314                    vec![args.source_range],
3315                )));
3316            };
3317            sketch_state.solver_constraints.push(constraint);
3318            #[cfg(feature = "artifact-graph")]
3319            {
3320                let constraint = crate::front::Constraint::Vertical(Vertical {
3321                    line: unsolved.object_id,
3322                });
3323                sketch_state.sketch_constraints.push(constraint_id);
3324                track_constraint(constraint_id, constraint, exec_state, &args);
3325            }
3326            Ok(KclValue::none())
3327        }
3328        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3329            "line's x and y coordinates of both start and end must be vars".to_owned(),
3330            vec![args.source_range],
3331        ))),
3332    }
3333}