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