Skip to main content

kcl_lib/std/
constraints.rs

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