Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::ops::ControlFlow;
5
6use indexmap::IndexMap;
7use kcl_error::CompilationError;
8use kcl_error::SourceRange;
9use kittycad_modeling_cmds::units::UnitLength;
10use serde::Serialize;
11
12use crate::ExecOutcome;
13use crate::ExecutorContext;
14use crate::KclError;
15use crate::KclErrorWithOutputs;
16use crate::Program;
17use crate::collections::AhashIndexSet;
18use crate::exec::WarningLevel;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::fmt::format_number_literal;
28use crate::front::Angle;
29use crate::front::ArcCtor;
30use crate::front::CircleCtor;
31use crate::front::Distance;
32use crate::front::FixedPoint;
33use crate::front::Freedom;
34use crate::front::LinesEqualLength;
35use crate::front::Parallel;
36use crate::front::Perpendicular;
37use crate::front::PointCtor;
38use crate::front::Tangent;
39use crate::frontend::api::Error;
40use crate::frontend::api::Expr;
41use crate::frontend::api::FileId;
42use crate::frontend::api::Number;
43use crate::frontend::api::ObjectId;
44use crate::frontend::api::ObjectKind;
45use crate::frontend::api::Plane;
46use crate::frontend::api::ProjectId;
47use crate::frontend::api::SceneGraph;
48use crate::frontend::api::SceneGraphDelta;
49use crate::frontend::api::SourceDelta;
50use crate::frontend::api::SourceRef;
51use crate::frontend::api::Version;
52use crate::frontend::modify::find_defined_names;
53use crate::frontend::modify::next_free_name;
54use crate::frontend::modify::next_free_name_with_padding;
55use crate::frontend::sketch::Coincident;
56use crate::frontend::sketch::Constraint;
57use crate::frontend::sketch::Diameter;
58use crate::frontend::sketch::ExistingSegmentCtor;
59use crate::frontend::sketch::Horizontal;
60use crate::frontend::sketch::LineCtor;
61use crate::frontend::sketch::Point2d;
62use crate::frontend::sketch::Radius;
63use crate::frontend::sketch::Segment;
64use crate::frontend::sketch::SegmentCtor;
65use crate::frontend::sketch::SketchApi;
66use crate::frontend::sketch::SketchCtor;
67use crate::frontend::sketch::Vertical;
68use crate::frontend::traverse::MutateBodyItem;
69use crate::frontend::traverse::TraversalReturn;
70use crate::frontend::traverse::Visitor;
71use crate::frontend::traverse::dfs_mut;
72use crate::parsing::ast::types as ast;
73use crate::pretty::NumericSuffix;
74use crate::std::constraints::LinesAtAngleKind;
75use crate::walk::NodeMut;
76use crate::walk::Visitable;
77
78pub(crate) mod api;
79pub(crate) mod modify;
80pub(crate) mod sketch;
81mod traverse;
82pub(crate) mod trim;
83
84struct ArcSizeConstraintParams {
85    points: Vec<ObjectId>,
86    function_name: &'static str,
87    value: f64,
88    units: NumericSuffix,
89    constraint_type_name: &'static str,
90}
91
92const POINT_FN: &str = "point";
93const POINT_AT_PARAM: &str = "at";
94const LINE_FN: &str = "line";
95const LINE_START_PARAM: &str = "start";
96const LINE_END_PARAM: &str = "end";
97const ARC_FN: &str = "arc";
98const ARC_START_PARAM: &str = "start";
99const ARC_END_PARAM: &str = "end";
100const ARC_CENTER_PARAM: &str = "center";
101const CIRCLE_FN: &str = "circle";
102const CIRCLE_START_PARAM: &str = "start";
103const CIRCLE_CENTER_PARAM: &str = "center";
104
105const COINCIDENT_FN: &str = "coincident";
106const DIAMETER_FN: &str = "diameter";
107const DISTANCE_FN: &str = "distance";
108const FIXED_FN: &str = "fixed";
109const ANGLE_FN: &str = "angle";
110const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
111const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
112const EQUAL_LENGTH_FN: &str = "equalLength";
113const HORIZONTAL_FN: &str = "horizontal";
114const RADIUS_FN: &str = "radius";
115const TANGENT_FN: &str = "tangent";
116const VERTICAL_FN: &str = "vertical";
117
118const LINE_PROPERTY_START: &str = "start";
119const LINE_PROPERTY_END: &str = "end";
120
121const ARC_PROPERTY_START: &str = "start";
122const ARC_PROPERTY_END: &str = "end";
123const ARC_PROPERTY_CENTER: &str = "center";
124const CIRCLE_PROPERTY_START: &str = "start";
125const CIRCLE_PROPERTY_CENTER: &str = "center";
126
127const CONSTRUCTION_PARAM: &str = "construction";
128
129#[derive(Debug, Clone, Copy)]
130enum EditDeleteKind {
131    Edit,
132    DeleteNonSketch,
133}
134
135impl EditDeleteKind {
136    /// Returns true if this edit is any type of deletion.
137    fn is_delete(&self) -> bool {
138        match self {
139            EditDeleteKind::Edit => false,
140            EditDeleteKind::DeleteNonSketch => true,
141        }
142    }
143
144    fn to_change_kind(self) -> ChangeKind {
145        match self {
146            EditDeleteKind::Edit => ChangeKind::Edit,
147            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy)]
153enum ChangeKind {
154    Add,
155    Edit,
156    Delete,
157    None,
158}
159
160#[derive(Debug, Clone, Serialize, ts_rs::TS)]
161#[ts(export, export_to = "FrontendApi.ts")]
162#[serde(tag = "type")]
163pub enum SetProgramOutcome {
164    #[serde(rename_all = "camelCase")]
165    Success {
166        scene_graph: Box<SceneGraph>,
167        exec_outcome: Box<ExecOutcome>,
168    },
169    #[serde(rename_all = "camelCase")]
170    ExecFailure { error: Box<KclErrorWithOutputs> },
171}
172
173#[derive(Debug, Clone)]
174pub struct FrontendState {
175    program: Program,
176    scene_graph: SceneGraph,
177    /// Stores the last known freedom value for each point object.
178    /// This allows us to preserve freedom values when freedom analysis isn't run.
179    point_freedom_cache: HashMap<ObjectId, Freedom>,
180}
181
182impl Default for FrontendState {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl FrontendState {
189    pub fn new() -> Self {
190        Self {
191            program: Program::empty(),
192            scene_graph: SceneGraph {
193                project: ProjectId(0),
194                file: FileId(0),
195                version: Version(0),
196                objects: Default::default(),
197                settings: Default::default(),
198                sketch_mode: Default::default(),
199            },
200            point_freedom_cache: HashMap::new(),
201        }
202    }
203
204    /// Get a reference to the scene graph
205    pub fn scene_graph(&self) -> &SceneGraph {
206        &self.scene_graph
207    }
208
209    pub fn default_length_unit(&self) -> UnitLength {
210        self.program
211            .meta_settings()
212            .ok()
213            .flatten()
214            .map(|settings| settings.default_length_units)
215            .unwrap_or(UnitLength::Millimeters)
216    }
217}
218
219impl SketchApi for FrontendState {
220    async fn execute_mock(
221        &mut self,
222        ctx: &ExecutorContext,
223        _version: Version,
224        sketch: ObjectId,
225    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
226        let mut truncated_program = self.program.clone();
227        self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
228
229        // Execute.
230        let outcome = ctx
231            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
232            .await
233            .map_err(|err| Error {
234                msg: err.error.message().to_owned(),
235            })?;
236        let new_source = source_from_ast(&self.program.ast);
237        let src_delta = SourceDelta { text: new_source };
238        // MockConfig::default() has freedom_analysis: true
239        let outcome = self.update_state_after_exec(outcome, true);
240        let scene_graph_delta = SceneGraphDelta {
241            new_graph: self.scene_graph.clone(),
242            new_objects: Default::default(),
243            invalidates_ids: false,
244            exec_outcome: outcome,
245        };
246        Ok((src_delta, scene_graph_delta))
247    }
248
249    async fn new_sketch(
250        &mut self,
251        ctx: &ExecutorContext,
252        _project: ProjectId,
253        _file: FileId,
254        _version: Version,
255        args: SketchCtor,
256    ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
257        // TODO: Check version.
258
259        let mut new_ast = self.program.ast.clone();
260        // Create updated KCL source from args.
261        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on)?;
262        let mut defined_names = find_defined_names(&new_ast);
263        let is_face_of_expr = matches!(
264            &plane_ast,
265            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
266        );
267        if is_face_of_expr {
268            let face_name =
269                next_free_name_with_padding("face", &defined_names).map_err(|err| Error { msg: err.to_string() })?;
270            let face_decl = ast::VariableDeclaration::new(
271                ast::VariableDeclarator::new(&face_name, plane_ast),
272                ast::ItemVisibility::Default,
273                ast::VariableKind::Const,
274            );
275            new_ast
276                .body
277                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
278                    face_decl,
279                ))));
280            defined_names.insert(face_name.clone());
281            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
282        }
283        let sketch_ast = ast::SketchBlock {
284            arguments: vec![ast::LabeledArg {
285                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
286                arg: plane_ast,
287            }],
288            body: Default::default(),
289            is_being_edited: false,
290            non_code_meta: Default::default(),
291            digest: None,
292        };
293        // Ensure that we allow experimental features since the sketch block
294        // won't work without it.
295        new_ast.set_experimental_features(Some(WarningLevel::Allow));
296        // Add a sketch block as a variable declaration directly, avoiding
297        // source-range mutation on a no-src node.
298        let sketch_name =
299            next_free_name_with_padding("sketch", &defined_names).map_err(|err| Error { msg: err.to_string() })?;
300        let sketch_decl = ast::VariableDeclaration::new(
301            ast::VariableDeclarator::new(
302                &sketch_name,
303                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
304            ),
305            ast::ItemVisibility::Default,
306            ast::VariableKind::Const,
307        );
308        new_ast
309            .body
310            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
311                sketch_decl,
312            ))));
313        // Convert to string source to create real source ranges.
314        let new_source = source_from_ast(&new_ast);
315        // Parse the new source.
316        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
317        if !errors.is_empty() {
318            return Err(Error {
319                msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
320            });
321        }
322        let Some(new_program) = new_program else {
323            return Err(Error {
324                msg: "No AST produced after adding sketch".to_owned(),
325            });
326        };
327
328        // Make sure to only set this if there are no errors.
329        self.program = new_program.clone();
330
331        // We need to do an engine execute so that the plane object gets created
332        // and is cached.
333        let outcome = ctx.run_with_caching(new_program.clone()).await.map_err(|err| Error {
334            msg: err.error.message().to_owned(),
335        })?;
336        let freedom_analysis_ran = true;
337
338        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
339
340        let Some(sketch_id) = self
341            .scene_graph
342            .objects
343            .iter()
344            .filter_map(|object| match object.kind {
345                ObjectKind::Sketch(_) => Some(object.id),
346                _ => None,
347            })
348            .max_by_key(|id| id.0)
349        else {
350            return Err(Error {
351                msg: "No objects in scene graph after adding sketch".to_owned(),
352            });
353        };
354        // Store the object in the scene.
355        self.scene_graph.sketch_mode = Some(sketch_id);
356
357        let src_delta = SourceDelta { text: new_source };
358        let scene_graph_delta = SceneGraphDelta {
359            new_graph: self.scene_graph.clone(),
360            invalidates_ids: false,
361            new_objects: vec![sketch_id],
362            exec_outcome: outcome,
363        };
364        Ok((src_delta, scene_graph_delta, sketch_id))
365    }
366
367    async fn edit_sketch(
368        &mut self,
369        ctx: &ExecutorContext,
370        _project: ProjectId,
371        _file: FileId,
372        _version: Version,
373        sketch: ObjectId,
374    ) -> api::Result<SceneGraphDelta> {
375        // TODO: Check version.
376
377        // Look up existing sketch.
378        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
379            msg: format!("Sketch not found: {sketch:?}"),
380        })?;
381        let ObjectKind::Sketch(_) = &sketch_object.kind else {
382            return Err(Error {
383                msg: format!("Object is not a sketch: {sketch_object:?}"),
384            });
385        };
386
387        // Enter sketch mode by setting the sketch_mode.
388        self.scene_graph.sketch_mode = Some(sketch);
389
390        // Truncate after the sketch block for mock execution.
391        let mut truncated_program = self.program.clone();
392        self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
393
394        // Execute in mock mode to ensure state is up to date. The caller will
395        // want freedom analysis to display segments correctly.
396        let outcome = ctx
397            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
398            .await
399            .map_err(|err| {
400                // TODO: sketch-api: Yeah, this needs to change. We need to
401                // return the full error.
402                Error {
403                    msg: err.error.message().to_owned(),
404                }
405            })?;
406
407        // MockConfig::default() has freedom_analysis: true
408        let outcome = self.update_state_after_exec(outcome, true);
409        let scene_graph_delta = SceneGraphDelta {
410            new_graph: self.scene_graph.clone(),
411            invalidates_ids: false,
412            new_objects: Vec::new(),
413            exec_outcome: outcome,
414        };
415        Ok(scene_graph_delta)
416    }
417
418    async fn exit_sketch(
419        &mut self,
420        ctx: &ExecutorContext,
421        _version: Version,
422        sketch: ObjectId,
423    ) -> api::Result<SceneGraph> {
424        // TODO: Check version.
425        #[cfg(not(target_arch = "wasm32"))]
426        let _ = sketch;
427        #[cfg(target_arch = "wasm32")]
428        if self.scene_graph.sketch_mode != Some(sketch) {
429            web_sys::console::warn_1(
430                &format!(
431                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
432                    &self.scene_graph.sketch_mode
433                )
434                .into(),
435            );
436        }
437        self.scene_graph.sketch_mode = None;
438
439        // Execute.
440        let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
441            // TODO: sketch-api: Yeah, this needs to change. We need to
442            // return the full error.
443            Error {
444                msg: err.error.message().to_owned(),
445            }
446        })?;
447
448        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
449        self.update_state_after_exec(outcome, false);
450
451        Ok(self.scene_graph.clone())
452    }
453
454    async fn delete_sketch(
455        &mut self,
456        ctx: &ExecutorContext,
457        _version: Version,
458        sketch: ObjectId,
459    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
460        // TODO: Check version.
461
462        let mut new_ast = self.program.ast.clone();
463
464        // Look up existing sketch.
465        let sketch_id = sketch;
466        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
467            msg: format!("Sketch not found: {sketch:?}"),
468        })?;
469        let ObjectKind::Sketch(_) = &sketch_object.kind else {
470            return Err(Error {
471                msg: format!("Object is not a sketch: {sketch_object:?}"),
472            });
473        };
474
475        // Modify the AST to remove the sketch.
476        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
477
478        self.execute_after_delete_sketch(ctx, &mut new_ast).await
479    }
480
481    async fn add_segment(
482        &mut self,
483        ctx: &ExecutorContext,
484        _version: Version,
485        sketch: ObjectId,
486        segment: SegmentCtor,
487        _label: Option<String>,
488    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
489        // TODO: Check version.
490        match segment {
491            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
492            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
493            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
494            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
495        }
496    }
497
498    async fn edit_segments(
499        &mut self,
500        ctx: &ExecutorContext,
501        _version: Version,
502        sketch: ObjectId,
503        segments: Vec<ExistingSegmentCtor>,
504    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
505        // TODO: Check version.
506        let mut new_ast = self.program.ast.clone();
507        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
508
509        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
510        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
511        for segment in &segments {
512            segment_ids_edited.insert(segment.id);
513        }
514
515        // Preprocess segments into a final_edits vector to handle if segments contains:
516        // - edit start point of line1 (as SegmentCtor::Point)
517        // - edit end point of line1 (as SegmentCtor::Point)
518        //
519        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
520        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
521        //
522        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
523        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
524        // so the above example would result in a single line1 edit:
525        // - the first start point edit creates a new line edit entry in final_edits
526        // - the second end point edit finds this line edit and mutates the end position only.
527        //
528        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
529        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
530
531        for segment in segments {
532            let segment_id = segment.id;
533            match segment.ctor {
534                SegmentCtor::Point(ctor) => {
535                    // Find the owner, if any (point -> line / arc)
536                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
537                        && let ObjectKind::Segment { segment } = &segment_object.kind
538                        && let Segment::Point(point) = segment
539                        && let Some(owner_id) = point.owner
540                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
541                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
542                    {
543                        match owner_segment {
544                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
545                                if let Some(existing) = final_edits.get_mut(&owner_id) {
546                                    let SegmentCtor::Line(line_ctor) = existing else {
547                                        return Err(Error {
548                                            msg: format!("Internal: Expected line ctor for owner: {owner_object:?}"),
549                                        });
550                                    };
551                                    // Line owner is already in final_edits -> apply this point edit
552                                    if line.start == segment_id {
553                                        line_ctor.start = ctor.position;
554                                    } else {
555                                        line_ctor.end = ctor.position;
556                                    }
557                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
558                                    // Line owner is not in final_edits yet -> create it
559                                    let mut line_ctor = line_ctor.clone();
560                                    if line.start == segment_id {
561                                        line_ctor.start = ctor.position;
562                                    } else {
563                                        line_ctor.end = ctor.position;
564                                    }
565                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
566                                } else {
567                                    // This should never run..
568                                    return Err(Error {
569                                        msg: format!("Internal: Line does not have line ctor: {owner_object:?}"),
570                                    });
571                                }
572                                continue;
573                            }
574                            Segment::Arc(arc)
575                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
576                            {
577                                if let Some(existing) = final_edits.get_mut(&owner_id) {
578                                    let SegmentCtor::Arc(arc_ctor) = existing else {
579                                        return Err(Error {
580                                            msg: format!("Internal: Expected arc ctor for owner: {owner_object:?}"),
581                                        });
582                                    };
583                                    if arc.start == segment_id {
584                                        arc_ctor.start = ctor.position;
585                                    } else if arc.end == segment_id {
586                                        arc_ctor.end = ctor.position;
587                                    } else {
588                                        arc_ctor.center = ctor.position;
589                                    }
590                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
591                                    let mut arc_ctor = arc_ctor.clone();
592                                    if arc.start == segment_id {
593                                        arc_ctor.start = ctor.position;
594                                    } else if arc.end == segment_id {
595                                        arc_ctor.end = ctor.position;
596                                    } else {
597                                        arc_ctor.center = ctor.position;
598                                    }
599                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
600                                } else {
601                                    return Err(Error {
602                                        msg: format!("Internal: Arc does not have arc ctor: {owner_object:?}"),
603                                    });
604                                }
605                                continue;
606                            }
607                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
608                                if let Some(existing) = final_edits.get_mut(&owner_id) {
609                                    let SegmentCtor::Circle(circle_ctor) = existing else {
610                                        return Err(Error {
611                                            msg: format!("Internal: Expected circle ctor for owner: {owner_object:?}"),
612                                        });
613                                    };
614                                    if circle.start == segment_id {
615                                        circle_ctor.start = ctor.position;
616                                    } else {
617                                        circle_ctor.center = ctor.position;
618                                    }
619                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
620                                    let mut circle_ctor = circle_ctor.clone();
621                                    if circle.start == segment_id {
622                                        circle_ctor.start = ctor.position;
623                                    } else {
624                                        circle_ctor.center = ctor.position;
625                                    }
626                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
627                                } else {
628                                    return Err(Error {
629                                        msg: format!("Internal: Circle does not have circle ctor: {owner_object:?}"),
630                                    });
631                                }
632                                continue;
633                            }
634                            _ => {}
635                        }
636                    }
637
638                    // No owner, it's an individual point
639                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
640                }
641                SegmentCtor::Line(ctor) => {
642                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
643                }
644                SegmentCtor::Arc(ctor) => {
645                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
646                }
647                SegmentCtor::Circle(ctor) => {
648                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
649                }
650            }
651        }
652
653        for (segment_id, ctor) in final_edits {
654            match ctor {
655                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment_id, ctor)?,
656                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment_id, ctor)?,
657                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment_id, ctor)?,
658                SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment_id, ctor)?,
659            }
660        }
661        self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
662            .await
663    }
664
665    async fn delete_objects(
666        &mut self,
667        ctx: &ExecutorContext,
668        _version: Version,
669        sketch: ObjectId,
670        constraint_ids: Vec<ObjectId>,
671        segment_ids: Vec<ObjectId>,
672    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
673        // TODO: Check version.
674
675        // Deduplicate IDs.
676        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
677        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
678
679        // If a point is owned by a Line/Arc, we want to delete the owner, which will
680        // also delete the point, as well as other points that are owned by the owner.
681        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
682
683        for segment_id in segment_ids_set.iter().copied() {
684            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
685                && let ObjectKind::Segment { segment } = &segment_object.kind
686                && let Segment::Point(point) = segment
687                && let Some(owner_id) = point.owner
688                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
689                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
690                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
691            {
692                // segment is owned -> delete the owner
693                resolved_segment_ids_to_delete.insert(owner_id);
694            } else {
695                // segment is not owned by anything -> can be deleted
696                resolved_segment_ids_to_delete.insert(segment_id);
697            }
698        }
699        let referenced_constraint_ids = self.find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)?;
700
701        let mut new_ast = self.program.ast.clone();
702
703        for constraint_id in referenced_constraint_ids {
704            if constraint_ids_set.contains(&constraint_id) {
705                continue;
706            }
707
708            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
709                msg: format!("Constraint not found: {constraint_id:?}"),
710            })?;
711            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
712                return Err(Error {
713                    msg: format!("Object is not a constraint: {constraint_object:?}"),
714                });
715            };
716
717            match constraint {
718                Constraint::LinesEqualLength(lines_equal_length) => {
719                    let remaining_lines = lines_equal_length
720                        .lines
721                        .iter()
722                        .copied()
723                        .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
724                        .collect::<Vec<_>>();
725
726                    // Equal length constraint is only valid with at least 2 lines
727                    if remaining_lines.len() >= 2 {
728                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)?;
729                    } else {
730                        constraint_ids_set.insert(constraint_id);
731                    }
732                }
733                _ => {
734                    // All other constraint types: if referenced by a segment -> delete the constraint
735                    constraint_ids_set.insert(constraint_id);
736                }
737            }
738        }
739
740        for constraint_id in constraint_ids_set {
741            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
742        }
743        for segment_id in resolved_segment_ids_to_delete {
744            self.delete_segment(&mut new_ast, sketch, segment_id)?;
745        }
746
747        self.execute_after_edit(
748            ctx,
749            sketch,
750            Default::default(),
751            EditDeleteKind::DeleteNonSketch,
752            &mut new_ast,
753        )
754        .await
755    }
756
757    async fn add_constraint(
758        &mut self,
759        ctx: &ExecutorContext,
760        _version: Version,
761        sketch: ObjectId,
762        constraint: Constraint,
763    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
764        // TODO: Check version.
765
766        // Save the original state as a backup - we'll restore it if anything fails
767        let original_program = self.program.clone();
768        let original_scene_graph = self.scene_graph.clone();
769
770        let mut new_ast = self.program.ast.clone();
771        let sketch_block_range = match constraint {
772            Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
773            Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
774            Constraint::Fixed(fixed) => self.add_fixed_constraints(sketch, fixed.points, &mut new_ast).await?,
775            Constraint::HorizontalDistance(distance) => {
776                self.add_horizontal_distance(sketch, distance, &mut new_ast).await?
777            }
778            Constraint::VerticalDistance(distance) => {
779                self.add_vertical_distance(sketch, distance, &mut new_ast).await?
780            }
781            Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
782            Constraint::LinesEqualLength(lines_equal_length) => {
783                self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
784                    .await?
785            }
786            Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
787            Constraint::Perpendicular(perpendicular) => {
788                self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
789            }
790            Constraint::Radius(radius) => self.add_radius(sketch, radius, &mut new_ast).await?,
791            Constraint::Diameter(diameter) => self.add_diameter(sketch, diameter, &mut new_ast).await?,
792            Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
793            Constraint::Angle(lines_at_angle) => self.add_angle(sketch, lines_at_angle, &mut new_ast).await?,
794            Constraint::Tangent(tangent) => self.add_tangent(sketch, tangent, &mut new_ast).await?,
795        };
796
797        let result = self
798            .execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
799            .await;
800
801        // If execution failed, restore the original state to prevent corruption
802        if result.is_err() {
803            self.program = original_program;
804            self.scene_graph = original_scene_graph;
805        }
806
807        result
808    }
809
810    async fn chain_segment(
811        &mut self,
812        ctx: &ExecutorContext,
813        version: Version,
814        sketch: ObjectId,
815        previous_segment_end_point_id: ObjectId,
816        segment: SegmentCtor,
817        _label: Option<String>,
818    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
819        // TODO: Check version.
820
821        // First, add the segment (line) to get its start point ID
822        let SegmentCtor::Line(line_ctor) = segment else {
823            return Err(Error {
824                msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
825            });
826        };
827
828        // Add the line segment first - this updates self.program and self.scene_graph
829        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
830
831        // Find the new line's start point ID from the updated scene graph
832        // add_line updates self.scene_graph, so we can use that
833        let new_line_id = first_scene_delta
834            .new_objects
835            .iter()
836            .find(|&obj_id| {
837                let obj = self.scene_graph.objects.get(obj_id.0);
838                if let Some(obj) = obj {
839                    matches!(
840                        &obj.kind,
841                        ObjectKind::Segment {
842                            segment: Segment::Line(_)
843                        }
844                    )
845                } else {
846                    false
847                }
848            })
849            .ok_or_else(|| Error {
850                msg: "Failed to find new line segment in scene graph".to_string(),
851            })?;
852
853        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
854            msg: format!("New line object not found: {new_line_id:?}"),
855        })?;
856
857        let ObjectKind::Segment {
858            segment: new_line_segment,
859        } = &new_line_obj.kind
860        else {
861            return Err(Error {
862                msg: format!("Object is not a segment: {new_line_obj:?}"),
863            });
864        };
865
866        let Segment::Line(new_line) = new_line_segment else {
867            return Err(Error {
868                msg: format!("Segment is not a line: {new_line_segment:?}"),
869            });
870        };
871
872        let new_line_start_point_id = new_line.start;
873
874        // Now add the coincident constraint between the previous end point and the new line's start point.
875        let coincident = Coincident {
876            segments: vec![previous_segment_end_point_id, new_line_start_point_id],
877        };
878
879        let (final_src_delta, final_scene_delta) = self
880            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
881            .await?;
882
883        // Combine new objects from the line addition and the constraint addition.
884        // Both add_line and add_constraint now populate new_objects correctly.
885        let mut combined_new_objects = first_scene_delta.new_objects.clone();
886        combined_new_objects.extend(final_scene_delta.new_objects);
887
888        let scene_graph_delta = SceneGraphDelta {
889            new_graph: self.scene_graph.clone(),
890            invalidates_ids: false,
891            new_objects: combined_new_objects,
892            exec_outcome: final_scene_delta.exec_outcome,
893        };
894
895        Ok((final_src_delta, scene_graph_delta))
896    }
897
898    async fn edit_constraint(
899        &mut self,
900        ctx: &ExecutorContext,
901        _version: Version,
902        sketch: ObjectId,
903        constraint_id: ObjectId,
904        value_expression: String,
905    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
906        // TODO: Check version.
907        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
908            msg: format!("Object not found: {constraint_id:?}"),
909        })?;
910        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
911            return Err(Error {
912                msg: format!("Object is not a constraint: {constraint_id:?}"),
913            });
914        }
915
916        let mut new_ast = self.program.ast.clone();
917
918        // Parse the expression string into an AST node.
919        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| Error { msg: e.to_string() })?;
920        if !errors.is_empty() {
921            return Err(Error {
922                msg: format!("Error parsing value expression: {errors:?}"),
923            });
924        }
925        let mut parsed = parsed.ok_or_else(|| Error {
926            msg: "No AST produced from value expression".to_string(),
927        })?;
928        if parsed.ast.body.is_empty() {
929            return Err(Error {
930                msg: "Empty value expression".to_string(),
931            });
932        }
933        let first = parsed.ast.body.remove(0);
934        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
935            return Err(Error {
936                msg: "Value expression must be a simple expression".to_string(),
937            });
938        };
939
940        let new_value: ast::BinaryPart = expr_stmt
941            .inner
942            .expression
943            .try_into()
944            .map_err(|e: String| Error { msg: e })?;
945
946        self.mutate_ast(
947            &mut new_ast,
948            constraint_id,
949            AstMutateCommand::EditConstraintValue { value: new_value },
950        )?;
951
952        self.execute_after_edit(ctx, sketch, Default::default(), EditDeleteKind::Edit, &mut new_ast)
953            .await
954    }
955
956    /// Splitting a segment means creating a new segment, editing the old one, and then
957    /// migrating a bunch of the constraints from the original segment to the new one
958    /// (i.e. deleting them and re-adding them on the other segment).
959    ///
960    /// To keep this efficient we require as few executions as possible: we create the
961    /// new segment first (to get its id), then do all edits and new constraints, and
962    /// do all deletes at the end (since deletes invalidate ids).
963    async fn batch_split_segment_operations(
964        &mut self,
965        ctx: &ExecutorContext,
966        _version: Version,
967        sketch: ObjectId,
968        edit_segments: Vec<ExistingSegmentCtor>,
969        add_constraints: Vec<Constraint>,
970        delete_constraint_ids: Vec<ObjectId>,
971        _new_segment_info: sketch::NewSegmentInfo,
972    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
973        // TODO: Check version.
974        let mut new_ast = self.program.ast.clone();
975        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
976
977        // Step 1: Edit segments
978        for segment in edit_segments {
979            segment_ids_edited.insert(segment.id);
980            match segment.ctor {
981                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
982                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
983                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
984                SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment.id, ctor)?,
985            }
986        }
987
988        // Step 2: Add all constraints
989        for constraint in add_constraints {
990            match constraint {
991                Constraint::Coincident(coincident) => {
992                    self.add_coincident(sketch, coincident, &mut new_ast).await?;
993                }
994                Constraint::Distance(distance) => {
995                    self.add_distance(sketch, distance, &mut new_ast).await?;
996                }
997                Constraint::Fixed(fixed) => {
998                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast).await?;
999                }
1000                Constraint::HorizontalDistance(distance) => {
1001                    self.add_horizontal_distance(sketch, distance, &mut new_ast).await?;
1002                }
1003                Constraint::VerticalDistance(distance) => {
1004                    self.add_vertical_distance(sketch, distance, &mut new_ast).await?;
1005                }
1006                Constraint::Horizontal(horizontal) => {
1007                    self.add_horizontal(sketch, horizontal, &mut new_ast).await?;
1008                }
1009                Constraint::LinesEqualLength(lines_equal_length) => {
1010                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1011                        .await?;
1012                }
1013                Constraint::Parallel(parallel) => {
1014                    self.add_parallel(sketch, parallel, &mut new_ast).await?;
1015                }
1016                Constraint::Perpendicular(perpendicular) => {
1017                    self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?;
1018                }
1019                Constraint::Vertical(vertical) => {
1020                    self.add_vertical(sketch, vertical, &mut new_ast).await?;
1021                }
1022                Constraint::Diameter(diameter) => {
1023                    self.add_diameter(sketch, diameter, &mut new_ast).await?;
1024                }
1025                Constraint::Radius(radius) => {
1026                    self.add_radius(sketch, radius, &mut new_ast).await?;
1027                }
1028                Constraint::Angle(angle) => {
1029                    self.add_angle(sketch, angle, &mut new_ast).await?;
1030                }
1031                Constraint::Tangent(tangent) => {
1032                    self.add_tangent(sketch, tangent, &mut new_ast).await?;
1033                }
1034            }
1035        }
1036
1037        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1038        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1039
1040        let has_constraint_deletions = !constraint_ids_set.is_empty();
1041        for constraint_id in constraint_ids_set {
1042            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
1043        }
1044
1045        // Step 4: Execute once at the end
1046        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1047        // But we'll manually set invalidates_ids: true if we deleted constraints
1048        let (source_delta, mut scene_graph_delta) = self
1049            .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
1050            .await?;
1051
1052        // If we deleted constraints, set invalidates_ids: true
1053        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1054        if has_constraint_deletions {
1055            scene_graph_delta.invalidates_ids = true;
1056        }
1057
1058        Ok((source_delta, scene_graph_delta))
1059    }
1060
1061    async fn batch_tail_cut_operations(
1062        &mut self,
1063        ctx: &ExecutorContext,
1064        _version: Version,
1065        sketch: ObjectId,
1066        edit_segments: Vec<ExistingSegmentCtor>,
1067        add_constraints: Vec<Constraint>,
1068        delete_constraint_ids: Vec<ObjectId>,
1069    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1070        let mut new_ast = self.program.ast.clone();
1071        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1072
1073        // Step 1: Edit segments (usually a single segment for tail cut)
1074        for segment in edit_segments {
1075            segment_ids_edited.insert(segment.id);
1076            match segment.ctor {
1077                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
1078                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
1079                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
1080                SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment.id, ctor)?,
1081            }
1082        }
1083
1084        // Step 2: Add coincident constraints
1085        for constraint in add_constraints {
1086            match constraint {
1087                Constraint::Coincident(coincident) => {
1088                    self.add_coincident(sketch, coincident, &mut new_ast).await?;
1089                }
1090                other => {
1091                    return Err(Error {
1092                        msg: format!("unsupported constraint in tail cut batch: {other:?}"),
1093                    });
1094                }
1095            }
1096        }
1097
1098        // Step 3: Delete constraints (if any)
1099        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1100
1101        let has_constraint_deletions = !constraint_ids_set.is_empty();
1102        for constraint_id in constraint_ids_set {
1103            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
1104        }
1105
1106        // Step 4: Single execute_after_edit
1107        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1108        // But we'll manually set invalidates_ids: true if we deleted constraints
1109        let (source_delta, mut scene_graph_delta) = self
1110            .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
1111            .await?;
1112
1113        // If we deleted constraints, set invalidates_ids: true
1114        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1115        if has_constraint_deletions {
1116            scene_graph_delta.invalidates_ids = true;
1117        }
1118
1119        Ok((source_delta, scene_graph_delta))
1120    }
1121}
1122
1123impl FrontendState {
1124    pub async fn hack_set_program(
1125        &mut self,
1126        ctx: &ExecutorContext,
1127        program: Program,
1128    ) -> api::Result<SetProgramOutcome> {
1129        self.program = program.clone();
1130
1131        // Execute so that the objects are updated and available for the next
1132        // API call.
1133        // This always uses engine execution (not mock) so that things are cached.
1134        // Engine execution now runs freedom analysis automatically.
1135        // Clear the freedom cache since IDs might have changed after direct editing
1136        // and we're about to run freedom analysis which will repopulate it.
1137        self.point_freedom_cache.clear();
1138        match ctx.run_with_caching(program).await {
1139            Ok(outcome) => {
1140                let outcome = self.update_state_after_exec(outcome, true);
1141                Ok(SetProgramOutcome::Success {
1142                    scene_graph: Box::new(self.scene_graph.clone()),
1143                    exec_outcome: Box::new(outcome),
1144                })
1145            }
1146            Err(mut err) => {
1147                // Don't return an error just because execution failed. Instead,
1148                // update state as much as possible.
1149                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1150                self.update_state_after_exec(outcome, true);
1151                err.scene_graph = Some(self.scene_graph.clone());
1152                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1153            }
1154        }
1155    }
1156
1157    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> api::Result<ExecOutcome> {
1158        if matches!(err.error, KclError::EngineHangup { .. }) {
1159            // It's not ideal to special-case this, but this error is very
1160            // common during development, and it causes confusing downstream
1161            // errors that have nothing to do with the actual problem.
1162            return Err(Error {
1163                msg: err.error.message().to_owned(),
1164            });
1165        }
1166
1167        let KclErrorWithOutputs {
1168            error,
1169            mut non_fatal,
1170            variables,
1171            #[cfg(feature = "artifact-graph")]
1172            operations,
1173            #[cfg(feature = "artifact-graph")]
1174            artifact_graph,
1175            #[cfg(feature = "artifact-graph")]
1176            scene_objects,
1177            #[cfg(feature = "artifact-graph")]
1178            source_range_to_object,
1179            #[cfg(feature = "artifact-graph")]
1180            var_solutions,
1181            filenames,
1182            default_planes,
1183            ..
1184        } = err;
1185
1186        if let Some(source_range) = error.source_ranges().first() {
1187            non_fatal.push(CompilationError::fatal(*source_range, error.get_message()));
1188        } else {
1189            non_fatal.push(CompilationError::fatal(SourceRange::synthetic(), error.get_message()));
1190        }
1191
1192        Ok(ExecOutcome {
1193            variables,
1194            filenames,
1195            #[cfg(feature = "artifact-graph")]
1196            operations,
1197            #[cfg(feature = "artifact-graph")]
1198            artifact_graph,
1199            #[cfg(feature = "artifact-graph")]
1200            scene_objects,
1201            #[cfg(feature = "artifact-graph")]
1202            source_range_to_object,
1203            #[cfg(feature = "artifact-graph")]
1204            var_solutions,
1205            errors: non_fatal,
1206            default_planes,
1207        })
1208    }
1209
1210    async fn add_point(
1211        &mut self,
1212        ctx: &ExecutorContext,
1213        sketch: ObjectId,
1214        ctor: PointCtor,
1215    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1216        // Create updated KCL source from args.
1217        let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1218        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1219            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1220            unlabeled: None,
1221            arguments: vec![ast::LabeledArg {
1222                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1223                arg: at_ast,
1224            }],
1225            digest: None,
1226            non_code_meta: Default::default(),
1227        })));
1228
1229        // Look up existing sketch.
1230        let sketch_id = sketch;
1231        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1232            #[cfg(target_arch = "wasm32")]
1233            web_sys::console::error_1(
1234                &format!(
1235                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1236                    &self.scene_graph.objects
1237                )
1238                .into(),
1239            );
1240            Error {
1241                msg: format!("Sketch not found: {sketch:?}"),
1242            }
1243        })?;
1244        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1245            return Err(Error {
1246                msg: format!("Object is not a sketch: {sketch_object:?}"),
1247            });
1248        };
1249        // Add the point to the AST of the sketch block.
1250        let mut new_ast = self.program.ast.clone();
1251        let (sketch_block_range, _) = self.mutate_ast(
1252            &mut new_ast,
1253            sketch_id,
1254            AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1255        )?;
1256        // Convert to string source to create real source ranges.
1257        let new_source = source_from_ast(&new_ast);
1258        // Parse the new KCL source.
1259        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1260        if !errors.is_empty() {
1261            return Err(Error {
1262                msg: format!("Error parsing KCL source after adding point: {errors:?}"),
1263            });
1264        }
1265        let Some(new_program) = new_program else {
1266            return Err(Error {
1267                msg: "No AST produced after adding point".to_string(),
1268            });
1269        };
1270
1271        let point_source_range =
1272            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1273                msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
1274            })?;
1275        #[cfg(not(feature = "artifact-graph"))]
1276        let _ = point_source_range;
1277
1278        // Make sure to only set this if there are no errors.
1279        self.program = new_program.clone();
1280
1281        // Truncate after the sketch block for mock execution.
1282        let mut truncated_program = new_program;
1283        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1284
1285        // Execute.
1286        let outcome = ctx
1287            .run_mock(
1288                &truncated_program,
1289                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1290            )
1291            .await
1292            .map_err(|err| {
1293                // TODO: sketch-api: Yeah, this needs to change. We need to
1294                // return the full error.
1295                Error {
1296                    msg: err.error.message().to_owned(),
1297                }
1298            })?;
1299
1300        #[cfg(not(feature = "artifact-graph"))]
1301        let new_object_ids = Vec::new();
1302        #[cfg(feature = "artifact-graph")]
1303        let new_object_ids = {
1304            let segment_id = outcome
1305                .source_range_to_object
1306                .get(&point_source_range)
1307                .copied()
1308                .ok_or_else(|| Error {
1309                    msg: format!("Source range of point not found: {point_source_range:?}"),
1310                })?;
1311            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1312                msg: format!("Segment not found: {segment_id:?}"),
1313            })?;
1314            let ObjectKind::Segment { segment } = &segment_object.kind else {
1315                return Err(Error {
1316                    msg: format!("Object is not a segment: {segment_object:?}"),
1317                });
1318            };
1319            let Segment::Point(_) = segment else {
1320                return Err(Error {
1321                    msg: format!("Segment is not a point: {segment:?}"),
1322                });
1323            };
1324            vec![segment_id]
1325        };
1326        let src_delta = SourceDelta { text: new_source };
1327        // Uses .no_freedom_analysis() so freedom_analysis: false
1328        let outcome = self.update_state_after_exec(outcome, false);
1329        let scene_graph_delta = SceneGraphDelta {
1330            new_graph: self.scene_graph.clone(),
1331            invalidates_ids: false,
1332            new_objects: new_object_ids,
1333            exec_outcome: outcome,
1334        };
1335        Ok((src_delta, scene_graph_delta))
1336    }
1337
1338    async fn add_line(
1339        &mut self,
1340        ctx: &ExecutorContext,
1341        sketch: ObjectId,
1342        ctor: LineCtor,
1343    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1344        // Create updated KCL source from args.
1345        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1346        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1347        let mut arguments = vec![
1348            ast::LabeledArg {
1349                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1350                arg: start_ast,
1351            },
1352            ast::LabeledArg {
1353                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1354                arg: end_ast,
1355            },
1356        ];
1357        // Add construction kwarg if construction is Some(true)
1358        if ctor.construction == Some(true) {
1359            arguments.push(ast::LabeledArg {
1360                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1361                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1362                    value: ast::LiteralValue::Bool(true),
1363                    raw: "true".to_string(),
1364                    digest: None,
1365                }))),
1366            });
1367        }
1368        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1369            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1370            unlabeled: None,
1371            arguments,
1372            digest: None,
1373            non_code_meta: Default::default(),
1374        })));
1375
1376        // Look up existing sketch.
1377        let sketch_id = sketch;
1378        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1379            msg: format!("Sketch not found: {sketch:?}"),
1380        })?;
1381        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1382            return Err(Error {
1383                msg: format!("Object is not a sketch: {sketch_object:?}"),
1384            });
1385        };
1386        // Add the line to the AST of the sketch block.
1387        let mut new_ast = self.program.ast.clone();
1388        let (sketch_block_range, _) = self.mutate_ast(
1389            &mut new_ast,
1390            sketch_id,
1391            AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1392        )?;
1393        // Convert to string source to create real source ranges.
1394        let new_source = source_from_ast(&new_ast);
1395        // Parse the new KCL source.
1396        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1397        if !errors.is_empty() {
1398            return Err(Error {
1399                msg: format!("Error parsing KCL source after adding line: {errors:?}"),
1400            });
1401        }
1402        let Some(new_program) = new_program else {
1403            return Err(Error {
1404                msg: "No AST produced after adding line".to_string(),
1405            });
1406        };
1407        let line_source_range =
1408            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1409                msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
1410            })?;
1411        #[cfg(not(feature = "artifact-graph"))]
1412        let _ = line_source_range;
1413
1414        // Make sure to only set this if there are no errors.
1415        self.program = new_program.clone();
1416
1417        // Truncate after the sketch block for mock execution.
1418        let mut truncated_program = new_program;
1419        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1420
1421        // Execute.
1422        let outcome = ctx
1423            .run_mock(
1424                &truncated_program,
1425                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1426            )
1427            .await
1428            .map_err(|err| {
1429                // TODO: sketch-api: Yeah, this needs to change. We need to
1430                // return the full error.
1431                Error {
1432                    msg: err.error.message().to_owned(),
1433                }
1434            })?;
1435
1436        #[cfg(not(feature = "artifact-graph"))]
1437        let new_object_ids = Vec::new();
1438        #[cfg(feature = "artifact-graph")]
1439        let new_object_ids = {
1440            let segment_id = outcome
1441                .source_range_to_object
1442                .get(&line_source_range)
1443                .copied()
1444                .ok_or_else(|| Error {
1445                    msg: format!("Source range of line not found: {line_source_range:?}"),
1446                })?;
1447            let segment_object = outcome.scene_object_by_id(segment_id).ok_or_else(|| Error {
1448                msg: format!("Segment not found: {segment_id:?}"),
1449            })?;
1450            let ObjectKind::Segment { segment } = &segment_object.kind else {
1451                return Err(Error {
1452                    msg: format!("Object is not a segment: {segment_object:?}"),
1453                });
1454            };
1455            let Segment::Line(line) = segment else {
1456                return Err(Error {
1457                    msg: format!("Segment is not a line: {segment:?}"),
1458                });
1459            };
1460            vec![line.start, line.end, segment_id]
1461        };
1462        let src_delta = SourceDelta { text: new_source };
1463        // Uses .no_freedom_analysis() so freedom_analysis: false
1464        let outcome = self.update_state_after_exec(outcome, false);
1465        let scene_graph_delta = SceneGraphDelta {
1466            new_graph: self.scene_graph.clone(),
1467            invalidates_ids: false,
1468            new_objects: new_object_ids,
1469            exec_outcome: outcome,
1470        };
1471        Ok((src_delta, scene_graph_delta))
1472    }
1473
1474    async fn add_arc(
1475        &mut self,
1476        ctx: &ExecutorContext,
1477        sketch: ObjectId,
1478        ctor: ArcCtor,
1479    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1480        // Create updated KCL source from args.
1481        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1482        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1483        let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1484        let mut arguments = vec![
1485            ast::LabeledArg {
1486                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1487                arg: start_ast,
1488            },
1489            ast::LabeledArg {
1490                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1491                arg: end_ast,
1492            },
1493            ast::LabeledArg {
1494                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1495                arg: center_ast,
1496            },
1497        ];
1498        // Add construction kwarg if construction is Some(true)
1499        if ctor.construction == Some(true) {
1500            arguments.push(ast::LabeledArg {
1501                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1502                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1503                    value: ast::LiteralValue::Bool(true),
1504                    raw: "true".to_string(),
1505                    digest: None,
1506                }))),
1507            });
1508        }
1509        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1510            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1511            unlabeled: None,
1512            arguments,
1513            digest: None,
1514            non_code_meta: Default::default(),
1515        })));
1516
1517        // Look up existing sketch.
1518        let sketch_id = sketch;
1519        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1520            msg: format!("Sketch not found: {sketch:?}"),
1521        })?;
1522        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1523            return Err(Error {
1524                msg: format!("Object is not a sketch: {sketch_object:?}"),
1525            });
1526        };
1527        // Add the arc to the AST of the sketch block.
1528        let mut new_ast = self.program.ast.clone();
1529        let (sketch_block_range, _) = self.mutate_ast(
1530            &mut new_ast,
1531            sketch_id,
1532            AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1533        )?;
1534        // Convert to string source to create real source ranges.
1535        let new_source = source_from_ast(&new_ast);
1536        // Parse the new KCL source.
1537        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1538        if !errors.is_empty() {
1539            return Err(Error {
1540                msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
1541            });
1542        }
1543        let Some(new_program) = new_program else {
1544            return Err(Error {
1545                msg: "No AST produced after adding arc".to_string(),
1546            });
1547        };
1548        let arc_source_range =
1549            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1550                msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
1551            })?;
1552        #[cfg(not(feature = "artifact-graph"))]
1553        let _ = arc_source_range;
1554
1555        // Make sure to only set this if there are no errors.
1556        self.program = new_program.clone();
1557
1558        // Truncate after the sketch block for mock execution.
1559        let mut truncated_program = new_program;
1560        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1561
1562        // Execute.
1563        let outcome = ctx
1564            .run_mock(
1565                &truncated_program,
1566                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1567            )
1568            .await
1569            .map_err(|err| {
1570                // TODO: sketch-api: Yeah, this needs to change. We need to
1571                // return the full error.
1572                Error {
1573                    msg: err.error.message().to_owned(),
1574                }
1575            })?;
1576
1577        #[cfg(not(feature = "artifact-graph"))]
1578        let new_object_ids = Vec::new();
1579        #[cfg(feature = "artifact-graph")]
1580        let new_object_ids = {
1581            let segment_id = outcome
1582                .source_range_to_object
1583                .get(&arc_source_range)
1584                .copied()
1585                .ok_or_else(|| Error {
1586                    msg: format!("Source range of arc not found: {arc_source_range:?}"),
1587                })?;
1588            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1589                msg: format!("Segment not found: {segment_id:?}"),
1590            })?;
1591            let ObjectKind::Segment { segment } = &segment_object.kind else {
1592                return Err(Error {
1593                    msg: format!("Object is not a segment: {segment_object:?}"),
1594                });
1595            };
1596            let Segment::Arc(arc) = segment else {
1597                return Err(Error {
1598                    msg: format!("Segment is not an arc: {segment:?}"),
1599                });
1600            };
1601            vec![arc.start, arc.end, arc.center, segment_id]
1602        };
1603        let src_delta = SourceDelta { text: new_source };
1604        // Uses .no_freedom_analysis() so freedom_analysis: false
1605        let outcome = self.update_state_after_exec(outcome, false);
1606        let scene_graph_delta = SceneGraphDelta {
1607            new_graph: self.scene_graph.clone(),
1608            invalidates_ids: false,
1609            new_objects: new_object_ids,
1610            exec_outcome: outcome,
1611        };
1612        Ok((src_delta, scene_graph_delta))
1613    }
1614
1615    async fn add_circle(
1616        &mut self,
1617        ctx: &ExecutorContext,
1618        sketch: ObjectId,
1619        ctor: CircleCtor,
1620    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1621        // Create updated KCL source from args.
1622        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1623        let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1624        let mut arguments = vec![
1625            ast::LabeledArg {
1626                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
1627                arg: start_ast,
1628            },
1629            ast::LabeledArg {
1630                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
1631                arg: center_ast,
1632            },
1633        ];
1634        // Add construction kwarg if construction is Some(true)
1635        if ctor.construction == Some(true) {
1636            arguments.push(ast::LabeledArg {
1637                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1638                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1639                    value: ast::LiteralValue::Bool(true),
1640                    raw: "true".to_string(),
1641                    digest: None,
1642                }))),
1643            });
1644        }
1645        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1646            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
1647            unlabeled: None,
1648            arguments,
1649            digest: None,
1650            non_code_meta: Default::default(),
1651        })));
1652
1653        // Look up existing sketch.
1654        let sketch_id = sketch;
1655        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1656            msg: format!("Sketch not found: {sketch:?}"),
1657        })?;
1658        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1659            return Err(Error {
1660                msg: format!("Object is not a sketch: {sketch_object:?}"),
1661            });
1662        };
1663        // Add the circle to the AST of the sketch block.
1664        let mut new_ast = self.program.ast.clone();
1665        let (sketch_block_range, _) = self.mutate_ast(
1666            &mut new_ast,
1667            sketch_id,
1668            AstMutateCommand::AddSketchBlockExprStmt { expr: circle_ast },
1669        )?;
1670        // Convert to string source to create real source ranges.
1671        let new_source = source_from_ast(&new_ast);
1672        // Parse the new KCL source.
1673        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1674        if !errors.is_empty() {
1675            return Err(Error {
1676                msg: format!("Error parsing KCL source after adding circle: {errors:?}"),
1677            });
1678        }
1679        let Some(new_program) = new_program else {
1680            return Err(Error {
1681                msg: "No AST produced after adding circle".to_string(),
1682            });
1683        };
1684        let circle_source_range =
1685            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1686                msg: format!("Source range of circle not found in sketch block: {sketch_block_range:?}; {err:?}"),
1687            })?;
1688        #[cfg(not(feature = "artifact-graph"))]
1689        let _ = circle_source_range;
1690
1691        // Make sure to only set this if there are no errors.
1692        self.program = new_program.clone();
1693
1694        // Truncate after the sketch block for mock execution.
1695        let mut truncated_program = new_program;
1696        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1697
1698        // Execute.
1699        let outcome = ctx
1700            .run_mock(
1701                &truncated_program,
1702                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1703            )
1704            .await
1705            .map_err(|err| Error {
1706                msg: err.error.message().to_owned(),
1707            })?;
1708
1709        #[cfg(not(feature = "artifact-graph"))]
1710        let new_object_ids = Vec::new();
1711        #[cfg(feature = "artifact-graph")]
1712        let new_object_ids = {
1713            let segment_id = outcome
1714                .source_range_to_object
1715                .get(&circle_source_range)
1716                .copied()
1717                .ok_or_else(|| Error {
1718                    msg: format!("Source range of circle not found: {circle_source_range:?}"),
1719                })?;
1720            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1721                msg: format!("Segment not found: {segment_id:?}"),
1722            })?;
1723            let ObjectKind::Segment { segment } = &segment_object.kind else {
1724                return Err(Error {
1725                    msg: format!("Object is not a segment: {segment_object:?}"),
1726                });
1727            };
1728            let Segment::Circle(circle) = segment else {
1729                return Err(Error {
1730                    msg: format!("Segment is not a circle: {segment:?}"),
1731                });
1732            };
1733            vec![circle.start, circle.center, segment_id]
1734        };
1735        let src_delta = SourceDelta { text: new_source };
1736        // Uses .no_freedom_analysis() so freedom_analysis: false
1737        let outcome = self.update_state_after_exec(outcome, false);
1738        let scene_graph_delta = SceneGraphDelta {
1739            new_graph: self.scene_graph.clone(),
1740            invalidates_ids: false,
1741            new_objects: new_object_ids,
1742            exec_outcome: outcome,
1743        };
1744        Ok((src_delta, scene_graph_delta))
1745    }
1746
1747    fn edit_point(
1748        &mut self,
1749        new_ast: &mut ast::Node<ast::Program>,
1750        sketch: ObjectId,
1751        point: ObjectId,
1752        ctor: PointCtor,
1753    ) -> api::Result<()> {
1754        // Create updated KCL source from args.
1755        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1756
1757        // Look up existing sketch.
1758        let sketch_id = sketch;
1759        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1760            msg: format!("Sketch not found: {sketch:?}"),
1761        })?;
1762        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1763            return Err(Error {
1764                msg: format!("Object is not a sketch: {sketch_object:?}"),
1765            });
1766        };
1767        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1768            msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1769        })?;
1770        // Look up existing point.
1771        let point_id = point;
1772        let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1773            msg: format!("Point not found in scene graph: point={point:?}"),
1774        })?;
1775        let ObjectKind::Segment {
1776            segment: Segment::Point(point),
1777        } = &point_object.kind
1778        else {
1779            return Err(Error {
1780                msg: format!("Object is not a point segment: {point_object:?}"),
1781            });
1782        };
1783
1784        // If the point is part of a line or arc, edit the line/arc instead.
1785        if let Some(owner_id) = point.owner {
1786            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1787                msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1788            })?;
1789            let ObjectKind::Segment { segment } = &owner_object.kind else {
1790                return Err(Error {
1791                    msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1792                });
1793            };
1794
1795            // Handle Line owner
1796            if let Segment::Line(line) = segment {
1797                let SegmentCtor::Line(line_ctor) = &line.ctor else {
1798                    return Err(Error {
1799                        msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1800                    });
1801                };
1802                let mut line_ctor = line_ctor.clone();
1803                // Which end of the line is this point?
1804                if line.start == point_id {
1805                    line_ctor.start = ctor.position;
1806                } else if line.end == point_id {
1807                    line_ctor.end = ctor.position;
1808                } else {
1809                    return Err(Error {
1810                        msg: format!(
1811                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1812                        ),
1813                    });
1814                }
1815                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1816            }
1817
1818            // Handle Arc owner
1819            if let Segment::Arc(arc) = segment {
1820                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1821                    return Err(Error {
1822                        msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1823                    });
1824                };
1825                let mut arc_ctor = arc_ctor.clone();
1826                // Which point of the arc is this? (center, start, or end)
1827                if arc.center == point_id {
1828                    arc_ctor.center = ctor.position;
1829                } else if arc.start == point_id {
1830                    arc_ctor.start = ctor.position;
1831                } else if arc.end == point_id {
1832                    arc_ctor.end = ctor.position;
1833                } else {
1834                    return Err(Error {
1835                        msg: format!(
1836                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1837                        ),
1838                    });
1839                }
1840                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1841            }
1842
1843            // Handle Circle owner
1844            if let Segment::Circle(circle) = segment {
1845                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
1846                    return Err(Error {
1847                        msg: format!("Internal: Owner of point does not have circle ctor: {owner_object:?}"),
1848                    });
1849                };
1850                let mut circle_ctor = circle_ctor.clone();
1851                if circle.center == point_id {
1852                    circle_ctor.center = ctor.position;
1853                } else if circle.start == point_id {
1854                    circle_ctor.start = ctor.position;
1855                } else {
1856                    return Err(Error {
1857                        msg: format!(
1858                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
1859                        ),
1860                    });
1861                }
1862                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
1863            }
1864
1865            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
1866            // (fall through to the point editing logic below)
1867        }
1868
1869        // Modify the point AST.
1870        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1871        Ok(())
1872    }
1873
1874    fn edit_line(
1875        &mut self,
1876        new_ast: &mut ast::Node<ast::Program>,
1877        sketch: ObjectId,
1878        line: ObjectId,
1879        ctor: LineCtor,
1880    ) -> api::Result<()> {
1881        // Create updated KCL source from args.
1882        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1883        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1884
1885        // Look up existing sketch.
1886        let sketch_id = sketch;
1887        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1888            msg: format!("Sketch not found: {sketch:?}"),
1889        })?;
1890        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1891            return Err(Error {
1892                msg: format!("Object is not a sketch: {sketch_object:?}"),
1893            });
1894        };
1895        sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1896            msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1897        })?;
1898        // Look up existing line.
1899        let line_id = line;
1900        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1901            msg: format!("Line not found in scene graph: line={line:?}"),
1902        })?;
1903        let ObjectKind::Segment { .. } = &line_object.kind else {
1904            return Err(Error {
1905                msg: format!("Object is not a segment: {line_object:?}"),
1906            });
1907        };
1908
1909        // Modify the line AST.
1910        self.mutate_ast(
1911            new_ast,
1912            line_id,
1913            AstMutateCommand::EditLine {
1914                start: new_start_ast,
1915                end: new_end_ast,
1916                construction: ctor.construction,
1917            },
1918        )?;
1919        Ok(())
1920    }
1921
1922    fn edit_arc(
1923        &mut self,
1924        new_ast: &mut ast::Node<ast::Program>,
1925        sketch: ObjectId,
1926        arc: ObjectId,
1927        ctor: ArcCtor,
1928    ) -> api::Result<()> {
1929        // Create updated KCL source from args.
1930        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1931        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1932        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1933
1934        // Look up existing sketch.
1935        let sketch_id = sketch;
1936        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1937            msg: format!("Sketch not found: {sketch:?}"),
1938        })?;
1939        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1940            return Err(Error {
1941                msg: format!("Object is not a sketch: {sketch_object:?}"),
1942            });
1943        };
1944        sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1945            msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1946        })?;
1947        // Look up existing arc.
1948        let arc_id = arc;
1949        let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1950            msg: format!("Arc not found in scene graph: arc={arc:?}"),
1951        })?;
1952        let ObjectKind::Segment { .. } = &arc_object.kind else {
1953            return Err(Error {
1954                msg: format!("Object is not a segment: {arc_object:?}"),
1955            });
1956        };
1957
1958        // Modify the arc AST.
1959        self.mutate_ast(
1960            new_ast,
1961            arc_id,
1962            AstMutateCommand::EditArc {
1963                start: new_start_ast,
1964                end: new_end_ast,
1965                center: new_center_ast,
1966                construction: ctor.construction,
1967            },
1968        )?;
1969        Ok(())
1970    }
1971
1972    fn edit_circle(
1973        &mut self,
1974        new_ast: &mut ast::Node<ast::Program>,
1975        sketch: ObjectId,
1976        circle: ObjectId,
1977        ctor: CircleCtor,
1978    ) -> api::Result<()> {
1979        // Create updated KCL source from args.
1980        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1981        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1982
1983        // Look up existing sketch.
1984        let sketch_id = sketch;
1985        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1986            msg: format!("Sketch not found: {sketch:?}"),
1987        })?;
1988        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1989            return Err(Error {
1990                msg: format!("Object is not a sketch: {sketch_object:?}"),
1991            });
1992        };
1993        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| Error {
1994            msg: format!("Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"),
1995        })?;
1996        // Look up existing circle.
1997        let circle_id = circle;
1998        let circle_object = self.scene_graph.objects.get(circle_id.0).ok_or_else(|| Error {
1999            msg: format!("Circle not found in scene graph: circle={circle:?}"),
2000        })?;
2001        let ObjectKind::Segment { .. } = &circle_object.kind else {
2002            return Err(Error {
2003                msg: format!("Object is not a segment: {circle_object:?}"),
2004            });
2005        };
2006
2007        // Modify the circle AST.
2008        self.mutate_ast(
2009            new_ast,
2010            circle_id,
2011            AstMutateCommand::EditCircle {
2012                start: new_start_ast,
2013                center: new_center_ast,
2014                construction: ctor.construction,
2015            },
2016        )?;
2017        Ok(())
2018    }
2019
2020    fn delete_segment(
2021        &mut self,
2022        new_ast: &mut ast::Node<ast::Program>,
2023        sketch: ObjectId,
2024        segment_id: ObjectId,
2025    ) -> api::Result<()> {
2026        // Look up existing sketch.
2027        let sketch_id = sketch;
2028        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2029            msg: format!("Sketch not found: {sketch:?}"),
2030        })?;
2031        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2032            return Err(Error {
2033                msg: format!("Object is not a sketch: {sketch_object:?}"),
2034            });
2035        };
2036        sketch
2037            .segments
2038            .iter()
2039            .find(|o| **o == segment_id)
2040            .ok_or_else(|| Error {
2041                msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
2042            })?;
2043        // Look up existing segment.
2044        let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
2045            msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
2046        })?;
2047        let ObjectKind::Segment { .. } = &segment_object.kind else {
2048            return Err(Error {
2049                msg: format!("Object is not a segment: {segment_object:?}"),
2050            });
2051        };
2052
2053        // Modify the AST to remove the segment.
2054        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2055        Ok(())
2056    }
2057
2058    fn delete_constraint(
2059        &mut self,
2060        new_ast: &mut ast::Node<ast::Program>,
2061        sketch: ObjectId,
2062        constraint_id: ObjectId,
2063    ) -> api::Result<()> {
2064        // Look up existing sketch.
2065        let sketch_id = sketch;
2066        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2067            msg: format!("Sketch not found: {sketch:?}"),
2068        })?;
2069        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2070            return Err(Error {
2071                msg: format!("Object is not a sketch: {sketch_object:?}"),
2072            });
2073        };
2074        sketch
2075            .constraints
2076            .iter()
2077            .find(|o| **o == constraint_id)
2078            .ok_or_else(|| Error {
2079                msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
2080            })?;
2081        // Look up existing constraint.
2082        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
2083            msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
2084        })?;
2085        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2086            return Err(Error {
2087                msg: format!("Object is not a constraint: {constraint_object:?}"),
2088            });
2089        };
2090
2091        // Modify the AST to remove the constraint.
2092        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2093        Ok(())
2094    }
2095
2096    /// updates the equalLength constraint with the given lines
2097    fn edit_equal_length_constraint(
2098        &mut self,
2099        new_ast: &mut ast::Node<ast::Program>,
2100        constraint_id: ObjectId,
2101        lines: Vec<ObjectId>,
2102    ) -> api::Result<()> {
2103        if lines.len() < 2 {
2104            return Err(Error {
2105                msg: format!(
2106                    "Lines equal length constraint must have at least 2 lines, got {}",
2107                    lines.len()
2108                ),
2109            });
2110        }
2111
2112        let line_asts = lines
2113            .iter()
2114            .map(|line_id| {
2115                let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2116                    msg: format!("Line not found: {line_id:?}"),
2117                })?;
2118                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2119                    return Err(Error {
2120                        msg: format!("Object is not a segment: {line_object:?}"),
2121                    });
2122                };
2123                let Segment::Line(_) = line_segment else {
2124                    return Err(Error {
2125                        msg: format!("Only lines can be made equal length: {line_object:?}"),
2126                    });
2127                };
2128
2129                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2130            })
2131            .collect::<Result<Vec<_>, _>>()?;
2132
2133        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2134            elements: line_asts,
2135            digest: None,
2136            non_code_meta: Default::default(),
2137        })));
2138
2139        self.mutate_ast(
2140            new_ast,
2141            constraint_id,
2142            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2143        )?;
2144        Ok(())
2145    }
2146
2147    async fn execute_after_edit(
2148        &mut self,
2149        ctx: &ExecutorContext,
2150        sketch: ObjectId,
2151        segment_ids_edited: AhashIndexSet<ObjectId>,
2152        edit_kind: EditDeleteKind,
2153        new_ast: &mut ast::Node<ast::Program>,
2154    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2155        // Convert to string source to create real source ranges.
2156        let new_source = source_from_ast(new_ast);
2157        // Parse the new KCL source.
2158        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2159        if !errors.is_empty() {
2160            return Err(Error {
2161                msg: format!("Error parsing KCL source after editing: {errors:?}"),
2162            });
2163        }
2164        let Some(new_program) = new_program else {
2165            return Err(Error {
2166                msg: "No AST produced after editing".to_string(),
2167            });
2168        };
2169
2170        // TODO: sketch-api: make sure to only set this if there are no errors.
2171        self.program = new_program.clone();
2172
2173        // Truncate after the sketch block for mock execution.
2174        let is_delete = edit_kind.is_delete();
2175        let truncated_program = {
2176            let mut truncated_program = new_program;
2177            self.only_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
2178            truncated_program
2179        };
2180
2181        #[cfg(not(feature = "artifact-graph"))]
2182        drop(segment_ids_edited);
2183
2184        // Execute.
2185        let mock_config = MockConfig {
2186            sketch_block_id: Some(sketch),
2187            freedom_analysis: is_delete,
2188            #[cfg(feature = "artifact-graph")]
2189            segment_ids_edited: segment_ids_edited.clone(),
2190            ..Default::default()
2191        };
2192        let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
2193            // TODO: sketch-api: Yeah, this needs to change. We need to
2194            // return the full error.
2195            Error {
2196                msg: err.error.message().to_owned(),
2197            }
2198        })?;
2199
2200        // Uses freedom_analysis: is_delete
2201        let outcome = self.update_state_after_exec(outcome, is_delete);
2202
2203        #[cfg(feature = "artifact-graph")]
2204        let new_source = {
2205            // Feed back sketch var solutions into the source.
2206            //
2207            // The interpreter is returning all var solutions from the sketch
2208            // block we're editing.
2209            let mut new_ast = self.program.ast.clone();
2210            for (var_range, value) in &outcome.var_solutions {
2211                let rounded = value.round(3);
2212                mutate_ast_node_by_source_range(
2213                    &mut new_ast,
2214                    *var_range,
2215                    AstMutateCommand::EditVarInitialValue { value: rounded },
2216                )?;
2217            }
2218            source_from_ast(&new_ast)
2219        };
2220
2221        let src_delta = SourceDelta { text: new_source };
2222        let scene_graph_delta = SceneGraphDelta {
2223            new_graph: self.scene_graph.clone(),
2224            invalidates_ids: is_delete,
2225            new_objects: Vec::new(),
2226            exec_outcome: outcome,
2227        };
2228        Ok((src_delta, scene_graph_delta))
2229    }
2230
2231    async fn execute_after_delete_sketch(
2232        &mut self,
2233        ctx: &ExecutorContext,
2234        new_ast: &mut ast::Node<ast::Program>,
2235    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2236        // Convert to string source to create real source ranges.
2237        let new_source = source_from_ast(new_ast);
2238        // Parse the new KCL source.
2239        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2240        if !errors.is_empty() {
2241            return Err(Error {
2242                msg: format!("Error parsing KCL source after editing: {errors:?}"),
2243            });
2244        }
2245        let Some(new_program) = new_program else {
2246            return Err(Error {
2247                msg: "No AST produced after editing".to_string(),
2248            });
2249        };
2250
2251        // Make sure to only set this if there are no errors.
2252        self.program = new_program.clone();
2253
2254        // We deleted the entire sketch block. It doesn't make sense to truncate
2255        // and execute only the sketch block. We execute the whole program with
2256        // a real engine.
2257
2258        // Execute.
2259        let outcome = ctx.run_with_caching(new_program).await.map_err(|err| {
2260            // TODO: sketch-api: Yeah, this needs to change. We need to
2261            // return the full error.
2262            Error {
2263                msg: err.error.message().to_owned(),
2264            }
2265        })?;
2266        let freedom_analysis_ran = true;
2267
2268        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2269
2270        let src_delta = SourceDelta { text: new_source };
2271        let scene_graph_delta = SceneGraphDelta {
2272            new_graph: self.scene_graph.clone(),
2273            invalidates_ids: true,
2274            new_objects: Vec::new(),
2275            exec_outcome: outcome,
2276        };
2277        Ok((src_delta, scene_graph_delta))
2278    }
2279
2280    /// Map a point object id into an AST reference expression for use in
2281    /// constraints. If the point is owned by a segment (line or arc), we
2282    /// reference the appropriate property on that segment (e.g. `line1.start`,
2283    /// `arc1.center`). Otherwise we reference the point directly.
2284    fn point_id_to_ast_reference(
2285        &self,
2286        point_id: ObjectId,
2287        new_ast: &mut ast::Node<ast::Program>,
2288    ) -> api::Result<ast::Expr> {
2289        let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
2290            msg: format!("Point not found: {point_id:?}"),
2291        })?;
2292        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2293            return Err(Error {
2294                msg: format!("Object is not a segment: {point_object:?}"),
2295            });
2296        };
2297        let Segment::Point(point) = point_segment else {
2298            return Err(Error {
2299                msg: format!("Only points are currently supported: {point_object:?}"),
2300            });
2301        };
2302
2303        if let Some(owner_id) = point.owner {
2304            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
2305                msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
2306            })?;
2307            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2308                return Err(Error {
2309                    msg: format!("Owner of point is not a segment: {owner_object:?}"),
2310                });
2311            };
2312
2313            match owner_segment {
2314                Segment::Line(line) => {
2315                    let property = if line.start == point_id {
2316                        LINE_PROPERTY_START
2317                    } else if line.end == point_id {
2318                        LINE_PROPERTY_END
2319                    } else {
2320                        return Err(Error {
2321                            msg: format!(
2322                                "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2323                            ),
2324                        });
2325                    };
2326                    get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
2327                }
2328                Segment::Arc(arc) => {
2329                    let property = if arc.start == point_id {
2330                        ARC_PROPERTY_START
2331                    } else if arc.end == point_id {
2332                        ARC_PROPERTY_END
2333                    } else if arc.center == point_id {
2334                        ARC_PROPERTY_CENTER
2335                    } else {
2336                        return Err(Error {
2337                            msg: format!(
2338                                "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2339                            ),
2340                        });
2341                    };
2342                    get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
2343                }
2344                Segment::Circle(circle) => {
2345                    let property = if circle.start == point_id {
2346                        CIRCLE_PROPERTY_START
2347                    } else if circle.center == point_id {
2348                        CIRCLE_PROPERTY_CENTER
2349                    } else {
2350                        return Err(Error {
2351                            msg: format!(
2352                                "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2353                            ),
2354                        });
2355                    };
2356                    get_or_insert_ast_reference(new_ast, &owner_object.source, "circle", Some(property))
2357                }
2358                _ => Err(Error {
2359                    msg: format!(
2360                        "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2361                    ),
2362                }),
2363            }
2364        } else {
2365            // Standalone point.
2366            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2367        }
2368    }
2369
2370    async fn add_coincident(
2371        &mut self,
2372        sketch: ObjectId,
2373        coincident: Coincident,
2374        new_ast: &mut ast::Node<ast::Program>,
2375    ) -> api::Result<SourceRange> {
2376        let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
2377            return Err(Error {
2378                msg: format!(
2379                    "Coincident constraint must have exactly 2 segments, got {}",
2380                    coincident.segments.len()
2381                ),
2382            });
2383        };
2384        let sketch_id = sketch;
2385
2386        // Get AST reference for first object (point or segment)
2387        let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
2388            msg: format!("Object not found: {seg0_id:?}"),
2389        })?;
2390        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
2391            return Err(Error {
2392                msg: format!("Object is not a segment: {seg0_object:?}"),
2393            });
2394        };
2395        let seg0_ast = match seg0_segment {
2396            Segment::Point(_) => {
2397                // Use the helper function which supports both Line and Arc owners
2398                self.point_id_to_ast_reference(seg0_id, new_ast)?
2399            }
2400            Segment::Line(_) => {
2401                // Reference the segment directly (for point-segment coincident)
2402                get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
2403            }
2404            Segment::Arc(_) => {
2405                // Reference the segment directly (for point-arc coincident)
2406                get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
2407            }
2408            Segment::Circle(_) => {
2409                // Reference the segment directly (for point-circle coincident)
2410                get_or_insert_ast_reference(new_ast, &seg0_object.source, "circle", None)?
2411            }
2412        };
2413
2414        // Get AST reference for second object (point or segment)
2415        let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
2416            msg: format!("Object not found: {seg1_id:?}"),
2417        })?;
2418        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
2419            return Err(Error {
2420                msg: format!("Object is not a segment: {seg1_object:?}"),
2421            });
2422        };
2423        let seg1_ast = match seg1_segment {
2424            Segment::Point(_) => {
2425                // Use the helper function which supports both Line and Arc owners
2426                self.point_id_to_ast_reference(seg1_id, new_ast)?
2427            }
2428            Segment::Line(_) => {
2429                // Reference the segment directly (for point-segment coincident)
2430                get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
2431            }
2432            Segment::Arc(_) => {
2433                // Reference the segment directly (for point-arc coincident)
2434                get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
2435            }
2436            Segment::Circle(_) => {
2437                // Reference the segment directly (for point-circle coincident)
2438                get_or_insert_ast_reference(new_ast, &seg1_object.source, "circle", None)?
2439            }
2440        };
2441
2442        // Create the coincident() call using shared helper.
2443        let coincident_ast = create_coincident_ast(seg0_ast, seg1_ast);
2444
2445        // Add the line to the AST of the sketch block.
2446        let (sketch_block_range, _) = self.mutate_ast(
2447            new_ast,
2448            sketch_id,
2449            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
2450        )?;
2451        Ok(sketch_block_range)
2452    }
2453
2454    async fn add_distance(
2455        &mut self,
2456        sketch: ObjectId,
2457        distance: Distance,
2458        new_ast: &mut ast::Node<ast::Program>,
2459    ) -> api::Result<SourceRange> {
2460        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2461            return Err(Error {
2462                msg: format!(
2463                    "Distance constraint must have exactly 2 points, got {}",
2464                    distance.points.len()
2465                ),
2466            });
2467        };
2468        let sketch_id = sketch;
2469
2470        // Map the runtime objects back to variable names.
2471        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2472        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2473
2474        // Create the distance() call.
2475        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2476            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
2477            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2478                ast::ArrayExpression {
2479                    elements: vec![pt0_ast, pt1_ast],
2480                    digest: None,
2481                    non_code_meta: Default::default(),
2482                },
2483            )))),
2484            arguments: Default::default(),
2485            digest: None,
2486            non_code_meta: Default::default(),
2487        })));
2488        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2489            left: distance_call_ast,
2490            operator: ast::BinaryOperator::Eq,
2491            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2492                value: ast::LiteralValue::Number {
2493                    value: distance.distance.value,
2494                    suffix: distance.distance.units,
2495                },
2496                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2497                    Error {
2498                        msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2499                    }
2500                })?,
2501                digest: None,
2502            }))),
2503            digest: None,
2504        })));
2505
2506        // Add the line to the AST of the sketch block.
2507        let (sketch_block_range, _) = self.mutate_ast(
2508            new_ast,
2509            sketch_id,
2510            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2511        )?;
2512        Ok(sketch_block_range)
2513    }
2514
2515    async fn add_angle(
2516        &mut self,
2517        sketch: ObjectId,
2518        angle: Angle,
2519        new_ast: &mut ast::Node<ast::Program>,
2520    ) -> api::Result<SourceRange> {
2521        let &[l0_id, l1_id] = angle.lines.as_slice() else {
2522            return Err(Error {
2523                msg: format!("Angle constraint must have exactly 2 lines, got {}", angle.lines.len()),
2524            });
2525        };
2526        let sketch_id = sketch;
2527
2528        // Map the runtime objects back to variable names.
2529        let line0_object = self.scene_graph.objects.get(l0_id.0).ok_or_else(|| Error {
2530            msg: format!("Line not found: {l0_id:?}"),
2531        })?;
2532        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2533            return Err(Error {
2534                msg: format!("Object is not a segment: {line0_object:?}"),
2535            });
2536        };
2537        let Segment::Line(_) = line0_segment else {
2538            return Err(Error {
2539                msg: format!("Only lines can be constrained to meet at an angle: {line0_object:?}",),
2540            });
2541        };
2542        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2543
2544        let line1_object = self.scene_graph.objects.get(l1_id.0).ok_or_else(|| Error {
2545            msg: format!("Line not found: {l1_id:?}"),
2546        })?;
2547        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2548            return Err(Error {
2549                msg: format!("Object is not a segment: {line1_object:?}"),
2550            });
2551        };
2552        let Segment::Line(_) = line1_segment else {
2553            return Err(Error {
2554                msg: format!("Only lines can be constrained to meet at an angle: {line1_object:?}",),
2555            });
2556        };
2557        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2558
2559        // Create the angle() call.
2560        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2561            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
2562            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2563                ast::ArrayExpression {
2564                    elements: vec![l0_ast, l1_ast],
2565                    digest: None,
2566                    non_code_meta: Default::default(),
2567                },
2568            )))),
2569            arguments: Default::default(),
2570            digest: None,
2571            non_code_meta: Default::default(),
2572        })));
2573        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2574            left: angle_call_ast,
2575            operator: ast::BinaryOperator::Eq,
2576            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2577                value: ast::LiteralValue::Number {
2578                    value: angle.angle.value,
2579                    suffix: angle.angle.units,
2580                },
2581                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| Error {
2582                    msg: format!("Could not format numeric suffix: {:?}", angle.angle.units),
2583                })?,
2584                digest: None,
2585            }))),
2586            digest: None,
2587        })));
2588
2589        // Add the line to the AST of the sketch block.
2590        let (sketch_block_range, _) = self.mutate_ast(
2591            new_ast,
2592            sketch_id,
2593            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
2594        )?;
2595        Ok(sketch_block_range)
2596    }
2597
2598    async fn add_tangent(
2599        &mut self,
2600        sketch: ObjectId,
2601        tangent: Tangent,
2602        new_ast: &mut ast::Node<ast::Program>,
2603    ) -> api::Result<SourceRange> {
2604        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
2605            return Err(Error {
2606                msg: format!(
2607                    "Tangent constraint must have exactly 2 segments, got {}",
2608                    tangent.input.len()
2609                ),
2610            });
2611        };
2612        let sketch_id = sketch;
2613
2614        let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
2615            msg: format!("Segment not found: {seg0_id:?}"),
2616        })?;
2617        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
2618            return Err(Error {
2619                msg: format!("Object is not a segment: {seg0_object:?}"),
2620            });
2621        };
2622        let seg0_ast = match seg0_segment {
2623            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
2624            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
2625            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "circle", None)?,
2626            _ => {
2627                return Err(Error {
2628                    msg: format!("Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"),
2629                });
2630            }
2631        };
2632
2633        let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
2634            msg: format!("Segment not found: {seg1_id:?}"),
2635        })?;
2636        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
2637            return Err(Error {
2638                msg: format!("Object is not a segment: {seg1_object:?}"),
2639            });
2640        };
2641        let seg1_ast = match seg1_segment {
2642            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
2643            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
2644            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "circle", None)?,
2645            _ => {
2646                return Err(Error {
2647                    msg: format!("Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"),
2648                });
2649            }
2650        };
2651
2652        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
2653        let (sketch_block_range, _) = self.mutate_ast(
2654            new_ast,
2655            sketch_id,
2656            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
2657        )?;
2658        Ok(sketch_block_range)
2659    }
2660
2661    async fn add_radius(
2662        &mut self,
2663        sketch: ObjectId,
2664        radius: Radius,
2665        new_ast: &mut ast::Node<ast::Program>,
2666    ) -> api::Result<SourceRange> {
2667        let params = ArcSizeConstraintParams {
2668            points: vec![radius.arc],
2669            function_name: RADIUS_FN,
2670            value: radius.radius.value,
2671            units: radius.radius.units,
2672            constraint_type_name: "Radius",
2673        };
2674        self.add_arc_size_constraint(sketch, params, new_ast).await
2675    }
2676
2677    async fn add_diameter(
2678        &mut self,
2679        sketch: ObjectId,
2680        diameter: Diameter,
2681        new_ast: &mut ast::Node<ast::Program>,
2682    ) -> api::Result<SourceRange> {
2683        let params = ArcSizeConstraintParams {
2684            points: vec![diameter.arc],
2685            function_name: DIAMETER_FN,
2686            value: diameter.diameter.value,
2687            units: diameter.diameter.units,
2688            constraint_type_name: "Diameter",
2689        };
2690        self.add_arc_size_constraint(sketch, params, new_ast).await
2691    }
2692
2693    async fn add_fixed_constraints(
2694        &mut self,
2695        sketch: ObjectId,
2696        points: Vec<FixedPoint>,
2697        new_ast: &mut ast::Node<ast::Program>,
2698    ) -> api::Result<SourceRange> {
2699        let mut sketch_block_range = None;
2700
2701        for fixed_point in points {
2702            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
2703            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
2704                .map_err(|err| Error { msg: err.to_string() })?;
2705
2706            let (range, _) = self.mutate_ast(
2707                new_ast,
2708                sketch,
2709                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
2710            )?;
2711            sketch_block_range = Some(range);
2712        }
2713
2714        sketch_block_range.ok_or_else(|| Error {
2715            msg: "Fixed constraint requires at least one point".to_owned(),
2716        })
2717    }
2718
2719    async fn add_arc_size_constraint(
2720        &mut self,
2721        sketch: ObjectId,
2722        params: ArcSizeConstraintParams,
2723        new_ast: &mut ast::Node<ast::Program>,
2724    ) -> api::Result<SourceRange> {
2725        let sketch_id = sketch;
2726
2727        // Constraint must have exactly 1 argument (arc segment)
2728        if params.points.len() != 1 {
2729            return Err(Error {
2730                msg: format!(
2731                    "{} constraint must have exactly 1 argument (an arc segment), got {}",
2732                    params.constraint_type_name,
2733                    params.points.len()
2734                ),
2735            });
2736        }
2737
2738        let arc_id = params.points[0];
2739        let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
2740            msg: format!("Arc segment not found: {arc_id:?}"),
2741        })?;
2742        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
2743            return Err(Error {
2744                msg: format!("Object is not a segment: {arc_object:?}"),
2745            });
2746        };
2747        let ref_type = match arc_segment {
2748            Segment::Arc(_) => "arc",
2749            Segment::Circle(_) => "circle",
2750            _ => {
2751                return Err(Error {
2752                    msg: format!(
2753                        "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
2754                        params.constraint_type_name
2755                    ),
2756                });
2757            }
2758        };
2759        // Reference the arc/circle segment directly
2760        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
2761
2762        // Create the function call.
2763        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2764            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
2765            unlabeled: Some(arc_ast),
2766            arguments: Default::default(),
2767            digest: None,
2768            non_code_meta: Default::default(),
2769        })));
2770        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2771            left: call_ast,
2772            operator: ast::BinaryOperator::Eq,
2773            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2774                value: ast::LiteralValue::Number {
2775                    value: params.value,
2776                    suffix: params.units,
2777                },
2778                raw: format_number_literal(params.value, params.units, None).map_err(|_| Error {
2779                    msg: format!("Could not format numeric suffix: {:?}", params.units),
2780                })?,
2781                digest: None,
2782            }))),
2783            digest: None,
2784        })));
2785
2786        // Add the line to the AST of the sketch block.
2787        let (sketch_block_range, _) = self.mutate_ast(
2788            new_ast,
2789            sketch_id,
2790            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
2791        )?;
2792        Ok(sketch_block_range)
2793    }
2794
2795    async fn add_horizontal_distance(
2796        &mut self,
2797        sketch: ObjectId,
2798        distance: Distance,
2799        new_ast: &mut ast::Node<ast::Program>,
2800    ) -> api::Result<SourceRange> {
2801        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2802            return Err(Error {
2803                msg: format!(
2804                    "Horizontal distance constraint must have exactly 2 points, got {}",
2805                    distance.points.len()
2806                ),
2807            });
2808        };
2809        let sketch_id = sketch;
2810
2811        // Map the runtime objects back to variable names.
2812        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2813        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2814
2815        // Create the horizontalDistance() call.
2816        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2817            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
2818            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2819                ast::ArrayExpression {
2820                    elements: vec![pt0_ast, pt1_ast],
2821                    digest: None,
2822                    non_code_meta: Default::default(),
2823                },
2824            )))),
2825            arguments: Default::default(),
2826            digest: None,
2827            non_code_meta: Default::default(),
2828        })));
2829        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2830            left: distance_call_ast,
2831            operator: ast::BinaryOperator::Eq,
2832            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2833                value: ast::LiteralValue::Number {
2834                    value: distance.distance.value,
2835                    suffix: distance.distance.units,
2836                },
2837                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2838                    Error {
2839                        msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2840                    }
2841                })?,
2842                digest: None,
2843            }))),
2844            digest: None,
2845        })));
2846
2847        // Add the line to the AST of the sketch block.
2848        let (sketch_block_range, _) = self.mutate_ast(
2849            new_ast,
2850            sketch_id,
2851            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2852        )?;
2853        Ok(sketch_block_range)
2854    }
2855
2856    async fn add_vertical_distance(
2857        &mut self,
2858        sketch: ObjectId,
2859        distance: Distance,
2860        new_ast: &mut ast::Node<ast::Program>,
2861    ) -> api::Result<SourceRange> {
2862        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2863            return Err(Error {
2864                msg: format!(
2865                    "Vertical distance constraint must have exactly 2 points, got {}",
2866                    distance.points.len()
2867                ),
2868            });
2869        };
2870        let sketch_id = sketch;
2871
2872        // Map the runtime objects back to variable names.
2873        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2874        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2875
2876        // Create the verticalDistance() call.
2877        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2878            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
2879            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2880                ast::ArrayExpression {
2881                    elements: vec![pt0_ast, pt1_ast],
2882                    digest: None,
2883                    non_code_meta: Default::default(),
2884                },
2885            )))),
2886            arguments: Default::default(),
2887            digest: None,
2888            non_code_meta: Default::default(),
2889        })));
2890        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2891            left: distance_call_ast,
2892            operator: ast::BinaryOperator::Eq,
2893            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2894                value: ast::LiteralValue::Number {
2895                    value: distance.distance.value,
2896                    suffix: distance.distance.units,
2897                },
2898                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2899                    Error {
2900                        msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2901                    }
2902                })?,
2903                digest: None,
2904            }))),
2905            digest: None,
2906        })));
2907
2908        // Add the line to the AST of the sketch block.
2909        let (sketch_block_range, _) = self.mutate_ast(
2910            new_ast,
2911            sketch_id,
2912            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2913        )?;
2914        Ok(sketch_block_range)
2915    }
2916
2917    async fn add_horizontal(
2918        &mut self,
2919        sketch: ObjectId,
2920        horizontal: Horizontal,
2921        new_ast: &mut ast::Node<ast::Program>,
2922    ) -> api::Result<SourceRange> {
2923        let sketch_id = sketch;
2924
2925        // Map the runtime objects back to variable names.
2926        let line_id = horizontal.line;
2927        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2928            msg: format!("Line not found: {line_id:?}"),
2929        })?;
2930        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2931            return Err(Error {
2932                msg: format!("Object is not a segment: {line_object:?}"),
2933            });
2934        };
2935        let Segment::Line(_) = line_segment else {
2936            return Err(Error {
2937                msg: format!("Only lines can be made horizontal: {line_object:?}"),
2938            });
2939        };
2940        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2941
2942        // Create the horizontal() call using shared helper.
2943        let horizontal_ast = create_horizontal_ast(line_ast);
2944
2945        // Add the line to the AST of the sketch block.
2946        let (sketch_block_range, _) = self.mutate_ast(
2947            new_ast,
2948            sketch_id,
2949            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
2950        )?;
2951        Ok(sketch_block_range)
2952    }
2953
2954    async fn add_lines_equal_length(
2955        &mut self,
2956        sketch: ObjectId,
2957        lines_equal_length: LinesEqualLength,
2958        new_ast: &mut ast::Node<ast::Program>,
2959    ) -> api::Result<SourceRange> {
2960        if lines_equal_length.lines.len() < 2 {
2961            return Err(Error {
2962                msg: format!(
2963                    "Lines equal length constraint must have at least 2 lines, got {}",
2964                    lines_equal_length.lines.len()
2965                ),
2966            });
2967        };
2968
2969        let sketch_id = sketch;
2970
2971        // Map the runtime objects back to variable names.
2972        let line_asts = lines_equal_length
2973            .lines
2974            .iter()
2975            .map(|line_id| {
2976                let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2977                    msg: format!("Line not found: {line_id:?}"),
2978                })?;
2979                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2980                    return Err(Error {
2981                        msg: format!("Object is not a segment: {line_object:?}"),
2982                    });
2983                };
2984                let Segment::Line(_) = line_segment else {
2985                    return Err(Error {
2986                        msg: format!("Only lines can be made equal length: {line_object:?}"),
2987                    });
2988                };
2989
2990                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2991            })
2992            .collect::<Result<Vec<_>, _>>()?;
2993
2994        // Create the equalLength() call using shared helper.
2995        let equal_length_ast = create_equal_length_ast(line_asts);
2996
2997        // Add the constraint to the AST of the sketch block.
2998        let (sketch_block_range, _) = self.mutate_ast(
2999            new_ast,
3000            sketch_id,
3001            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3002        )?;
3003        Ok(sketch_block_range)
3004    }
3005
3006    async fn add_parallel(
3007        &mut self,
3008        sketch: ObjectId,
3009        parallel: Parallel,
3010        new_ast: &mut ast::Node<ast::Program>,
3011    ) -> api::Result<SourceRange> {
3012        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
3013            .await
3014    }
3015
3016    async fn add_perpendicular(
3017        &mut self,
3018        sketch: ObjectId,
3019        perpendicular: Perpendicular,
3020        new_ast: &mut ast::Node<ast::Program>,
3021    ) -> api::Result<SourceRange> {
3022        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3023            .await
3024    }
3025
3026    async fn add_lines_at_angle_constraint(
3027        &mut self,
3028        sketch: ObjectId,
3029        angle_kind: LinesAtAngleKind,
3030        lines: Vec<ObjectId>,
3031        new_ast: &mut ast::Node<ast::Program>,
3032    ) -> api::Result<SourceRange> {
3033        let &[line0_id, line1_id] = lines.as_slice() else {
3034            return Err(Error {
3035                msg: format!(
3036                    "{} constraint must have exactly 2 lines, got {}",
3037                    angle_kind.to_function_name(),
3038                    lines.len()
3039                ),
3040            });
3041        };
3042
3043        let sketch_id = sketch;
3044
3045        // Map the runtime objects back to variable names.
3046        let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
3047            msg: format!("Line not found: {line0_id:?}"),
3048        })?;
3049        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3050            return Err(Error {
3051                msg: format!("Object is not a segment: {line0_object:?}"),
3052            });
3053        };
3054        let Segment::Line(_) = line0_segment else {
3055            return Err(Error {
3056                msg: format!(
3057                    "Only lines can be made {}: {line0_object:?}",
3058                    angle_kind.to_function_name()
3059                ),
3060            });
3061        };
3062        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3063
3064        let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
3065            msg: format!("Line not found: {line1_id:?}"),
3066        })?;
3067        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3068            return Err(Error {
3069                msg: format!("Object is not a segment: {line1_object:?}"),
3070            });
3071        };
3072        let Segment::Line(_) = line1_segment else {
3073            return Err(Error {
3074                msg: format!(
3075                    "Only lines can be made {}: {line1_object:?}",
3076                    angle_kind.to_function_name()
3077                ),
3078            });
3079        };
3080        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3081
3082        // Create the parallel() or perpendicular() call.
3083        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3084            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3085            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3086                ast::ArrayExpression {
3087                    elements: vec![line0_ast, line1_ast],
3088                    digest: None,
3089                    non_code_meta: Default::default(),
3090                },
3091            )))),
3092            arguments: Default::default(),
3093            digest: None,
3094            non_code_meta: Default::default(),
3095        })));
3096
3097        // Add the constraint to the AST of the sketch block.
3098        let (sketch_block_range, _) = self.mutate_ast(
3099            new_ast,
3100            sketch_id,
3101            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3102        )?;
3103        Ok(sketch_block_range)
3104    }
3105
3106    async fn add_vertical(
3107        &mut self,
3108        sketch: ObjectId,
3109        vertical: Vertical,
3110        new_ast: &mut ast::Node<ast::Program>,
3111    ) -> api::Result<SourceRange> {
3112        let sketch_id = sketch;
3113
3114        // Map the runtime objects back to variable names.
3115        let line_id = vertical.line;
3116        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
3117            msg: format!("Line not found: {line_id:?}"),
3118        })?;
3119        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3120            return Err(Error {
3121                msg: format!("Object is not a segment: {line_object:?}"),
3122            });
3123        };
3124        let Segment::Line(_) = line_segment else {
3125            return Err(Error {
3126                msg: format!("Only lines can be made vertical: {line_object:?}"),
3127            });
3128        };
3129        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
3130
3131        // Create the vertical() call using shared helper.
3132        let vertical_ast = create_vertical_ast(line_ast);
3133
3134        // Add the line to the AST of the sketch block.
3135        let (sketch_block_range, _) = self.mutate_ast(
3136            new_ast,
3137            sketch_id,
3138            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3139        )?;
3140        Ok(sketch_block_range)
3141    }
3142
3143    async fn execute_after_add_constraint(
3144        &mut self,
3145        ctx: &ExecutorContext,
3146        sketch_id: ObjectId,
3147        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
3148        new_ast: &mut ast::Node<ast::Program>,
3149    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
3150        // Convert to string source to create real source ranges.
3151        let new_source = source_from_ast(new_ast);
3152        // Parse the new KCL source.
3153        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
3154        if !errors.is_empty() {
3155            return Err(Error {
3156                msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
3157            });
3158        }
3159        let Some(new_program) = new_program else {
3160            return Err(Error {
3161                msg: "No AST produced after adding constraint".to_string(),
3162            });
3163        };
3164        #[cfg(feature = "artifact-graph")]
3165        let constraint_source_range =
3166            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
3167                msg: format!(
3168                    "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
3169                ),
3170            })?;
3171
3172        // Truncate after the sketch block for mock execution.
3173        // Use a clone so we don't mutate new_program yet
3174        let mut truncated_program = new_program.clone();
3175        self.only_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
3176
3177        // Execute - if this fails, we haven't modified self yet, so state is safe
3178        let outcome = ctx
3179            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
3180            .await
3181            .map_err(|err| {
3182                // TODO: sketch-api: Yeah, this needs to change. We need to
3183                // return the full error.
3184                Error {
3185                    msg: err.error.message().to_owned(),
3186                }
3187            })?;
3188
3189        #[cfg(not(feature = "artifact-graph"))]
3190        let new_object_ids = Vec::new();
3191        #[cfg(feature = "artifact-graph")]
3192        let new_object_ids = {
3193            // Extract the constraint ID from the execution outcome using source_range_to_object
3194            let constraint_id = outcome
3195                .source_range_to_object
3196                .get(&constraint_source_range)
3197                .copied()
3198                .ok_or_else(|| Error {
3199                    msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
3200                })?;
3201            vec![constraint_id]
3202        };
3203
3204        // Only now, after all operations succeeded, update self.program
3205        // This ensures state is only modified if everything succeeds
3206        self.program = new_program;
3207
3208        // Uses MockConfig::default() which has freedom_analysis: true
3209        let outcome = self.update_state_after_exec(outcome, true);
3210
3211        let src_delta = SourceDelta { text: new_source };
3212        let scene_graph_delta = SceneGraphDelta {
3213            new_graph: self.scene_graph.clone(),
3214            invalidates_ids: false,
3215            new_objects: new_object_ids,
3216            exec_outcome: outcome,
3217        };
3218        Ok((src_delta, scene_graph_delta))
3219    }
3220
3221    // Find constraints that reference the given segments.
3222    fn find_referenced_constraints(
3223        &self,
3224        sketch_id: ObjectId,
3225        segment_ids_set: &AhashIndexSet<ObjectId>,
3226    ) -> api::Result<AhashIndexSet<ObjectId>> {
3227        // Look up the sketch.
3228        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
3229            msg: format!("Sketch not found: {sketch_id:?}"),
3230        })?;
3231        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3232            return Err(Error {
3233                msg: format!("Object is not a sketch: {sketch_object:?}"),
3234            });
3235        };
3236        let mut constraint_ids_set = AhashIndexSet::default();
3237        for constraint_id in &sketch.constraints {
3238            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
3239                msg: format!("Constraint not found: {constraint_id:?}"),
3240            })?;
3241            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
3242                return Err(Error {
3243                    msg: format!("Object is not a constraint: {constraint_object:?}"),
3244                });
3245            };
3246            let depends_on_segment = match constraint {
3247                Constraint::Coincident(c) => c.segments.iter().any(|seg_id| {
3248                    // Check if the segment itself is being deleted
3249                    if segment_ids_set.contains(seg_id) {
3250                        return true;
3251                    }
3252                    // For points, also check if the owner line/arc is being deleted
3253                    let seg_object = self.scene_graph.objects.get(seg_id.0);
3254                    if let Some(obj) = seg_object
3255                        && let ObjectKind::Segment { segment } = &obj.kind
3256                        && let Segment::Point(pt) = segment
3257                        && let Some(owner_line_id) = pt.owner
3258                    {
3259                        return segment_ids_set.contains(&owner_line_id);
3260                    }
3261                    false
3262                }),
3263                Constraint::Distance(d) => d.points.iter().any(|pt_id| {
3264                    if segment_ids_set.contains(pt_id) {
3265                        return true;
3266                    }
3267                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3268                    if let Some(obj) = pt_object
3269                        && let ObjectKind::Segment { segment } = &obj.kind
3270                        && let Segment::Point(pt) = segment
3271                        && let Some(owner_line_id) = pt.owner
3272                    {
3273                        return segment_ids_set.contains(&owner_line_id);
3274                    }
3275                    false
3276                }),
3277                Constraint::Fixed(_) => false,
3278                Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
3279                Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
3280                Constraint::HorizontalDistance(d) => d.points.iter().any(|pt_id| {
3281                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3282                    if let Some(obj) = pt_object
3283                        && let ObjectKind::Segment { segment } = &obj.kind
3284                        && let Segment::Point(pt) = segment
3285                        && let Some(owner_line_id) = pt.owner
3286                    {
3287                        return segment_ids_set.contains(&owner_line_id);
3288                    }
3289                    false
3290                }),
3291                Constraint::VerticalDistance(d) => d.points.iter().any(|pt_id| {
3292                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3293                    if let Some(obj) = pt_object
3294                        && let ObjectKind::Segment { segment } = &obj.kind
3295                        && let Segment::Point(pt) = segment
3296                        && let Some(owner_line_id) = pt.owner
3297                    {
3298                        return segment_ids_set.contains(&owner_line_id);
3299                    }
3300                    false
3301                }),
3302                Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
3303                Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
3304                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
3305                    .lines
3306                    .iter()
3307                    .any(|line_id| segment_ids_set.contains(line_id)),
3308                Constraint::Parallel(parallel) => {
3309                    parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
3310                }
3311                Constraint::Perpendicular(perpendicular) => perpendicular
3312                    .lines
3313                    .iter()
3314                    .any(|line_id| segment_ids_set.contains(line_id)),
3315                Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
3316                Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
3317            };
3318            if depends_on_segment {
3319                constraint_ids_set.insert(*constraint_id);
3320            }
3321        }
3322        Ok(constraint_ids_set)
3323    }
3324
3325    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
3326        #[cfg(not(feature = "artifact-graph"))]
3327        {
3328            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
3329            outcome
3330        }
3331        #[cfg(feature = "artifact-graph")]
3332        {
3333            let mut outcome = outcome;
3334            let mut new_objects = std::mem::take(&mut outcome.scene_objects);
3335
3336            if freedom_analysis_ran {
3337                // When freedom analysis ran, replace the cache entirely with new values
3338                // Don't merge with old values since IDs might have changed
3339                self.point_freedom_cache.clear();
3340                for new_obj in &new_objects {
3341                    if let ObjectKind::Segment {
3342                        segment: crate::front::Segment::Point(point),
3343                    } = &new_obj.kind
3344                    {
3345                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
3346                    }
3347                }
3348                add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
3349                // Objects are already correct from the analysis, just use them as-is
3350                self.scene_graph.objects = new_objects;
3351            } else {
3352                // When freedom analysis didn't run, preserve old values and merge
3353                // Before replacing objects, extract and store freedom values from old objects
3354                for old_obj in &self.scene_graph.objects {
3355                    if let ObjectKind::Segment {
3356                        segment: crate::front::Segment::Point(point),
3357                    } = &old_obj.kind
3358                    {
3359                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
3360                    }
3361                }
3362
3363                // Update objects, preserving stored freedom values when new is Free (might be default)
3364                let mut updated_objects = Vec::with_capacity(new_objects.len());
3365                for new_obj in new_objects {
3366                    let mut obj = new_obj;
3367                    if let ObjectKind::Segment {
3368                        segment: crate::front::Segment::Point(point),
3369                    } = &mut obj.kind
3370                    {
3371                        let new_freedom = point.freedom;
3372                        // When freedom_analysis=false, new values are defaults (Free).
3373                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
3374                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
3375                        // Never preserve Conflict from cache - conflicts are transient and should only be set
3376                        // when there are actually unsatisfied constraints.
3377                        match new_freedom {
3378                            Freedom::Free => {
3379                                match self.point_freedom_cache.get(&obj.id).copied() {
3380                                    Some(Freedom::Conflict) => {
3381                                        // Don't preserve Conflict - conflicts are transient
3382                                        // Keep it as Free
3383                                    }
3384                                    Some(Freedom::Fixed) => {
3385                                        // Preserve Fixed cached value
3386                                        point.freedom = Freedom::Fixed;
3387                                    }
3388                                    Some(Freedom::Free) => {
3389                                        // If stored is also Free, keep Free (no change needed)
3390                                    }
3391                                    None => {
3392                                        // If no cached value, keep Free (default)
3393                                    }
3394                                }
3395                            }
3396                            Freedom::Fixed => {
3397                                // Use new value (already set)
3398                            }
3399                            Freedom::Conflict => {
3400                                // Use new value (already set)
3401                            }
3402                        }
3403                        // Store the new freedom value (even if it's Free, so we know it was set)
3404                        self.point_freedom_cache.insert(obj.id, point.freedom);
3405                    }
3406                    updated_objects.push(obj);
3407                }
3408
3409                add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
3410                self.scene_graph.objects = updated_objects;
3411            }
3412            outcome
3413        }
3414    }
3415
3416    fn only_sketch_block(
3417        &self,
3418        sketch_id: ObjectId,
3419        edit_kind: ChangeKind,
3420        ast: &mut ast::Node<ast::Program>,
3421    ) -> api::Result<()> {
3422        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
3423            msg: format!("Sketch not found: {sketch_id:?}"),
3424        })?;
3425        let ObjectKind::Sketch(_) = &sketch_object.kind else {
3426            return Err(Error {
3427                msg: format!("Object is not a sketch: {sketch_object:?}"),
3428            });
3429        };
3430        let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
3431        only_sketch_block(ast, sketch_block_range, edit_kind)
3432    }
3433
3434    fn mutate_ast(
3435        &mut self,
3436        ast: &mut ast::Node<ast::Program>,
3437        object_id: ObjectId,
3438        command: AstMutateCommand,
3439    ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
3440        let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
3441            msg: format!("Object not found: {object_id:?}"),
3442        })?;
3443        match &sketch_object.source {
3444            SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
3445            SourceRef::BackTrace { .. } => Err(Error {
3446                msg: "BackTrace source refs not supported yet".to_owned(),
3447            }),
3448        }
3449    }
3450}
3451
3452fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
3453    match source_ref {
3454        SourceRef::Simple { range } => Ok(*range),
3455        SourceRef::BackTrace { ranges } => {
3456            if ranges.len() != 1 {
3457                return Err(Error {
3458                    msg: format!(
3459                        "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
3460                        ranges.len(),
3461                    ),
3462                });
3463            }
3464            Ok(ranges[0])
3465        }
3466    }
3467}
3468
3469fn only_sketch_block(
3470    ast: &mut ast::Node<ast::Program>,
3471    sketch_block_range: SourceRange,
3472    edit_kind: ChangeKind,
3473) -> api::Result<()> {
3474    let r1 = sketch_block_range;
3475    let matches_range = |r2: SourceRange| -> bool {
3476        // We may have added items to the sketch block, so the end may not be an
3477        // exact match.
3478        match edit_kind {
3479            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
3480            // For edit, we don't know whether it grew or shrank.
3481            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
3482            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
3483            // No edit should be an exact match.
3484            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
3485        }
3486    };
3487    let mut found = false;
3488    for item in ast.body.iter_mut() {
3489        match item {
3490            ast::BodyItem::ImportStatement(_) => {}
3491            ast::BodyItem::ExpressionStatement(node) => {
3492                if matches_range(SourceRange::from(&*node))
3493                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
3494                {
3495                    sketch_block.is_being_edited = true;
3496                    found = true;
3497                    break;
3498                }
3499            }
3500            ast::BodyItem::VariableDeclaration(node) => {
3501                if matches_range(SourceRange::from(&node.declaration.init))
3502                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
3503                {
3504                    sketch_block.is_being_edited = true;
3505                    found = true;
3506                    break;
3507                }
3508            }
3509            ast::BodyItem::TypeDeclaration(_) => {}
3510            ast::BodyItem::ReturnStatement(node) => {
3511                if matches_range(SourceRange::from(&node.argument))
3512                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
3513                {
3514                    sketch_block.is_being_edited = true;
3515                    found = true;
3516                    break;
3517                }
3518            }
3519        }
3520    }
3521    if !found {
3522        return Err(Error {
3523            msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
3524        });
3525    }
3526
3527    Ok(())
3528}
3529
3530fn sketch_on_ast_expr(
3531    ast: &mut ast::Node<ast::Program>,
3532    scene_graph: &SceneGraph,
3533    on: &Plane,
3534) -> api::Result<ast::Expr> {
3535    match on {
3536        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
3537        Plane::Object(object_id) => {
3538            let on_object = scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
3539                msg: format!("Sketch plane object not found: {object_id:?}"),
3540            })?;
3541            #[cfg(feature = "artifact-graph")]
3542            {
3543                if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
3544                    return Ok(face_expr);
3545                }
3546            }
3547            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
3548        }
3549    }
3550}
3551
3552#[cfg(feature = "artifact-graph")]
3553fn sketch_face_of_scene_object_ast_expr(
3554    ast: &mut ast::Node<ast::Program>,
3555    on_object: &crate::front::Object,
3556) -> api::Result<Option<ast::Expr>> {
3557    let SourceRef::BackTrace { ranges } = &on_object.source else {
3558        return Ok(None);
3559    };
3560
3561    match &on_object.kind {
3562        ObjectKind::Wall(_) => {
3563            let [sweep_range, segment_range] = ranges.as_slice() else {
3564                return Err(Error {
3565                    msg: format!(
3566                        "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
3567                        ranges.len(),
3568                        on_object.artifact_id
3569                    ),
3570                });
3571            };
3572            let sweep_ref =
3573                get_or_insert_ast_reference(ast, &SourceRef::Simple { range: *sweep_range }, "solid", None)?;
3574            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
3575                return Err(Error {
3576                    msg: format!(
3577                        "Could not resolve sweep reference for selected wall: artifact_id={:?}",
3578                        on_object.artifact_id
3579                    ),
3580                });
3581            };
3582            let solid_name = solid_name_expr.name.name.clone();
3583            let solid_expr = ast_name_expr(solid_name.clone());
3584            let segment_ref =
3585                get_or_insert_ast_reference(ast, &SourceRef::Simple { range: *segment_range }, "line", None)?;
3586
3587            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
3588                let ast::Expr::Name(segment_name_expr) = segment_ref else {
3589                    return Err(Error {
3590                        msg: format!(
3591                            "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
3592                            on_object.artifact_id
3593                        ),
3594                    });
3595                };
3596                create_member_expression(
3597                    create_member_expression(ast_name_expr(region_name), "tags"),
3598                    &segment_name_expr.name.name,
3599                )
3600            } else {
3601                segment_ref
3602            };
3603
3604            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
3605        }
3606        ObjectKind::Cap(cap) => {
3607            let [range] = ranges.as_slice() else {
3608                return Err(Error {
3609                    msg: format!(
3610                        "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
3611                        ranges.len(),
3612                        on_object.artifact_id
3613                    ),
3614                });
3615            };
3616            let sweep_ref = get_or_insert_ast_reference(ast, &SourceRef::Simple { range: *range }, "solid", None)?;
3617            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
3618                return Err(Error {
3619                    msg: format!(
3620                        "Could not resolve sweep reference for selected cap: artifact_id={:?}",
3621                        on_object.artifact_id
3622                    ),
3623                });
3624            };
3625            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
3626            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
3627            let face_expr = match cap.kind {
3628                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
3629                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
3630            };
3631
3632            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
3633        }
3634        _ => Ok(None),
3635    }
3636}
3637
3638#[cfg(feature = "artifact-graph")]
3639fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
3640    let mut existing_artifact_ids = scene_objects
3641        .iter()
3642        .map(|object| object.artifact_id)
3643        .collect::<HashSet<_>>();
3644
3645    for artifact in artifact_graph.values() {
3646        match artifact {
3647            Artifact::Wall(wall) => {
3648                if existing_artifact_ids.contains(&wall.id) {
3649                    continue;
3650                }
3651
3652                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
3653                    Artifact::Segment(segment) => Some(segment),
3654                    _ => None,
3655                }) else {
3656                    continue;
3657                };
3658                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
3659                    Artifact::Sweep(sweep) => Some(sweep),
3660                    _ => None,
3661                }) else {
3662                    continue;
3663                };
3664                let source_segment = segment
3665                    .original_seg_id
3666                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
3667                    .and_then(|artifact| match artifact {
3668                        Artifact::Segment(segment) => Some(segment),
3669                        _ => None,
3670                    })
3671                    .unwrap_or(segment);
3672                let id = ObjectId(scene_objects.len());
3673                scene_objects.push(crate::front::Object {
3674                    id,
3675                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
3676                    label: Default::default(),
3677                    comments: Default::default(),
3678                    artifact_id: wall.id,
3679                    source: SourceRef::BackTrace {
3680                        ranges: vec![sweep.code_ref.range, source_segment.code_ref.range],
3681                    },
3682                });
3683                existing_artifact_ids.insert(wall.id);
3684            }
3685            Artifact::Cap(cap) => {
3686                if existing_artifact_ids.contains(&cap.id) {
3687                    continue;
3688                }
3689
3690                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
3691                    Artifact::Sweep(sweep) => Some(sweep),
3692                    _ => None,
3693                }) else {
3694                    continue;
3695                };
3696                let id = ObjectId(scene_objects.len());
3697                let kind = match cap.sub_type {
3698                    CapSubType::Start => crate::frontend::api::CapKind::Start,
3699                    CapSubType::End => crate::frontend::api::CapKind::End,
3700                };
3701                scene_objects.push(crate::front::Object {
3702                    id,
3703                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
3704                    label: Default::default(),
3705                    comments: Default::default(),
3706                    artifact_id: cap.id,
3707                    source: SourceRef::BackTrace {
3708                        ranges: vec![sweep.code_ref.range],
3709                    },
3710                });
3711                existing_artifact_ids.insert(cap.id);
3712            }
3713            _ => {}
3714        }
3715    }
3716}
3717
3718fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
3719    use crate::engine::PlaneName;
3720
3721    match name {
3722        PlaneName::Xy => ast_name_expr("XY".to_owned()),
3723        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
3724        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
3725        PlaneName::NegXy => negated_plane_ast_expr("XY"),
3726        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
3727        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
3728    }
3729}
3730
3731fn negated_plane_ast_expr(name: &str) -> ast::Expr {
3732    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
3733        ast::UnaryOperator::Neg,
3734        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
3735    )))
3736}
3737
3738#[cfg(feature = "artifact-graph")]
3739fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
3740    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3741        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
3742        unlabeled: Some(solid_expr),
3743        arguments: vec![ast::LabeledArg {
3744            label: Some(ast::Identifier::new("face")),
3745            arg: face_expr,
3746        }],
3747        digest: None,
3748        non_code_meta: Default::default(),
3749    })))
3750}
3751
3752#[cfg(feature = "artifact-graph")]
3753fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
3754    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
3755        return None;
3756    };
3757    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
3758        return None;
3759    };
3760    if !matches!(
3761        sweep_call.callee.name.name.as_str(),
3762        "extrude" | "revolve" | "sweep" | "loft"
3763    ) {
3764        return None;
3765    }
3766    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
3767        return None;
3768    };
3769    let candidate = region_name_expr.name.name.clone();
3770    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
3771        return None;
3772    };
3773    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
3774        return None;
3775    };
3776    if region_call.callee.name.name != "region" {
3777        return None;
3778    }
3779    Some(candidate)
3780}
3781
3782/// Return the AST expression referencing the variable at the given source ref.
3783/// If no such variable exists, insert a new variable declaration with the given
3784/// prefix.
3785///
3786/// This may return a complex expression referencing properties of the variable
3787/// (e.g., `line1.start`).
3788fn get_or_insert_ast_reference(
3789    ast: &mut ast::Node<ast::Program>,
3790    source_ref: &SourceRef,
3791    prefix: &str,
3792    property: Option<&str>,
3793) -> api::Result<ast::Expr> {
3794    let range = expect_single_source_range(source_ref)?;
3795    let command = AstMutateCommand::AddVariableDeclaration {
3796        prefix: prefix.to_owned(),
3797    };
3798    let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
3799    let AstMutateCommandReturn::Name(var_name) = ret else {
3800        return Err(Error {
3801            msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
3802        });
3803    };
3804    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
3805    let Some(property) = property else {
3806        // No property; just return the variable name.
3807        return Ok(var_expr);
3808    };
3809
3810    Ok(create_member_expression(var_expr, property))
3811}
3812
3813fn mutate_ast_node_by_source_range(
3814    ast: &mut ast::Node<ast::Program>,
3815    source_range: SourceRange,
3816    command: AstMutateCommand,
3817) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
3818    let mut context = AstMutateContext {
3819        source_range,
3820        command,
3821        defined_names_stack: Default::default(),
3822    };
3823    let control = dfs_mut(ast, &mut context);
3824    match control {
3825        ControlFlow::Continue(_) => Err(Error {
3826            msg: format!("Source range not found: {source_range:?}"),
3827        }),
3828        ControlFlow::Break(break_value) => break_value,
3829    }
3830}
3831
3832#[derive(Debug)]
3833struct AstMutateContext {
3834    source_range: SourceRange,
3835    command: AstMutateCommand,
3836    defined_names_stack: Vec<HashSet<String>>,
3837}
3838
3839#[derive(Debug)]
3840#[allow(clippy::large_enum_variant)]
3841enum AstMutateCommand {
3842    /// Add an expression statement to the sketch block.
3843    AddSketchBlockExprStmt {
3844        expr: ast::Expr,
3845    },
3846    AddVariableDeclaration {
3847        prefix: String,
3848    },
3849    EditPoint {
3850        at: ast::Expr,
3851    },
3852    EditLine {
3853        start: ast::Expr,
3854        end: ast::Expr,
3855        construction: Option<bool>,
3856    },
3857    EditArc {
3858        start: ast::Expr,
3859        end: ast::Expr,
3860        center: ast::Expr,
3861        construction: Option<bool>,
3862    },
3863    EditCircle {
3864        start: ast::Expr,
3865        center: ast::Expr,
3866        construction: Option<bool>,
3867    },
3868    EditConstraintValue {
3869        value: ast::BinaryPart,
3870    },
3871    EditCallUnlabeled {
3872        arg: ast::Expr,
3873    },
3874    #[cfg(feature = "artifact-graph")]
3875    EditVarInitialValue {
3876        value: Number,
3877    },
3878    DeleteNode,
3879}
3880
3881#[derive(Debug)]
3882enum AstMutateCommandReturn {
3883    None,
3884    Name(String),
3885}
3886
3887impl Visitor for AstMutateContext {
3888    type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
3889    type Continue = ();
3890
3891    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
3892        filter_and_process(self, node)
3893    }
3894
3895    fn finish(&mut self, node: NodeMut<'_>) {
3896        match &node {
3897            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
3898                self.defined_names_stack.pop();
3899            }
3900            _ => {}
3901        }
3902    }
3903}
3904
3905fn filter_and_process(
3906    ctx: &mut AstMutateContext,
3907    node: NodeMut,
3908) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
3909    let Ok(node_range) = SourceRange::try_from(&node) else {
3910        // Nodes that can't be converted to a range aren't interesting.
3911        return TraversalReturn::new_continue(());
3912    };
3913    // If we're adding a variable declaration, we need to look at variable
3914    // declaration expressions to see if it already has a variable, before
3915    // continuing. The variable declaration's source range won't match the
3916    // target; its init expression will.
3917    if let NodeMut::VariableDeclaration(var_decl) = &node {
3918        let expr_range = SourceRange::from(&var_decl.declaration.init);
3919        if expr_range == ctx.source_range {
3920            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
3921                // We found the variable declaration expression. It doesn't need
3922                // to be added.
3923                return TraversalReturn::new_break(Ok((
3924                    node_range,
3925                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
3926                )));
3927            }
3928            if let AstMutateCommand::DeleteNode = &ctx.command {
3929                // We found the variable declaration. Delete the variable along
3930                // with the segment.
3931                return TraversalReturn {
3932                    mutate_body_item: MutateBodyItem::Delete,
3933                    control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
3934                };
3935            }
3936        }
3937    }
3938
3939    if let NodeMut::Program(program) = &node {
3940        ctx.defined_names_stack.push(find_defined_names(*program));
3941    } else if let NodeMut::SketchBlock(block) = &node {
3942        ctx.defined_names_stack.push(find_defined_names(&block.body));
3943    }
3944
3945    // Make sure the node matches the source range.
3946    if node_range != ctx.source_range {
3947        return TraversalReturn::new_continue(());
3948    }
3949    process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
3950}
3951
3952fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
3953    match &ctx.command {
3954        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
3955            if let NodeMut::SketchBlock(sketch_block) = node {
3956                sketch_block
3957                    .body
3958                    .items
3959                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
3960                        inner: ast::ExpressionStatement {
3961                            expression: expr.clone(),
3962                            digest: None,
3963                        },
3964                        start: Default::default(),
3965                        end: Default::default(),
3966                        module_id: Default::default(),
3967                        outer_attrs: Default::default(),
3968                        pre_comments: Default::default(),
3969                        comment_start: Default::default(),
3970                    }));
3971                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3972            }
3973        }
3974        AstMutateCommand::AddVariableDeclaration { prefix } => {
3975            if let NodeMut::VariableDeclaration(inner) = node {
3976                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
3977            }
3978            if let NodeMut::ExpressionStatement(expr_stmt) = node {
3979                let empty_defined_names = HashSet::new();
3980                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
3981                let Ok(name) = next_free_name(prefix, defined_names) else {
3982                    // TODO: Return an error instead?
3983                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3984                };
3985                let mutate_node =
3986                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
3987                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
3988                        ast::ItemVisibility::Default,
3989                        ast::VariableKind::Const,
3990                    ))));
3991                return TraversalReturn {
3992                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
3993                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
3994                };
3995            }
3996        }
3997        AstMutateCommand::EditPoint { at } => {
3998            if let NodeMut::CallExpressionKw(call) = node {
3999                if call.callee.name.name != POINT_FN {
4000                    return TraversalReturn::new_continue(());
4001                }
4002                // Update the arguments.
4003                for labeled_arg in &mut call.arguments {
4004                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
4005                        labeled_arg.arg = at.clone();
4006                    }
4007                }
4008                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4009            }
4010        }
4011        AstMutateCommand::EditLine {
4012            start,
4013            end,
4014            construction,
4015        } => {
4016            if let NodeMut::CallExpressionKw(call) = node {
4017                if call.callee.name.name != LINE_FN {
4018                    return TraversalReturn::new_continue(());
4019                }
4020                // Update the arguments.
4021                for labeled_arg in &mut call.arguments {
4022                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
4023                        labeled_arg.arg = start.clone();
4024                    }
4025                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
4026                        labeled_arg.arg = end.clone();
4027                    }
4028                }
4029                // Handle construction kwarg
4030                if let Some(construction_value) = construction {
4031                    let construction_exists = call
4032                        .arguments
4033                        .iter()
4034                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4035                    if *construction_value {
4036                        // Add or update construction=true
4037                        if construction_exists {
4038                            // Update existing construction kwarg
4039                            for labeled_arg in &mut call.arguments {
4040                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4041                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4042                                        value: ast::LiteralValue::Bool(true),
4043                                        raw: "true".to_string(),
4044                                        digest: None,
4045                                    })));
4046                                }
4047                            }
4048                        } else {
4049                            // Add new construction kwarg
4050                            call.arguments.push(ast::LabeledArg {
4051                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4052                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4053                                    value: ast::LiteralValue::Bool(true),
4054                                    raw: "true".to_string(),
4055                                    digest: None,
4056                                }))),
4057                            });
4058                        }
4059                    } else {
4060                        // Remove construction kwarg if it exists
4061                        call.arguments
4062                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4063                    }
4064                }
4065                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4066            }
4067        }
4068        AstMutateCommand::EditArc {
4069            start,
4070            end,
4071            center,
4072            construction,
4073        } => {
4074            if let NodeMut::CallExpressionKw(call) = node {
4075                if call.callee.name.name != ARC_FN {
4076                    return TraversalReturn::new_continue(());
4077                }
4078                // Update the arguments.
4079                for labeled_arg in &mut call.arguments {
4080                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
4081                        labeled_arg.arg = start.clone();
4082                    }
4083                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
4084                        labeled_arg.arg = end.clone();
4085                    }
4086                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
4087                        labeled_arg.arg = center.clone();
4088                    }
4089                }
4090                // Handle construction kwarg
4091                if let Some(construction_value) = construction {
4092                    let construction_exists = call
4093                        .arguments
4094                        .iter()
4095                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4096                    if *construction_value {
4097                        // Add or update construction=true
4098                        if construction_exists {
4099                            // Update existing construction kwarg
4100                            for labeled_arg in &mut call.arguments {
4101                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4102                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4103                                        value: ast::LiteralValue::Bool(true),
4104                                        raw: "true".to_string(),
4105                                        digest: None,
4106                                    })));
4107                                }
4108                            }
4109                        } else {
4110                            // Add new construction kwarg
4111                            call.arguments.push(ast::LabeledArg {
4112                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4113                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4114                                    value: ast::LiteralValue::Bool(true),
4115                                    raw: "true".to_string(),
4116                                    digest: None,
4117                                }))),
4118                            });
4119                        }
4120                    } else {
4121                        // Remove construction kwarg if it exists
4122                        call.arguments
4123                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4124                    }
4125                }
4126                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4127            }
4128        }
4129        AstMutateCommand::EditCircle {
4130            start,
4131            center,
4132            construction,
4133        } => {
4134            if let NodeMut::CallExpressionKw(call) = node {
4135                if call.callee.name.name != CIRCLE_FN {
4136                    return TraversalReturn::new_continue(());
4137                }
4138                // Update the arguments.
4139                for labeled_arg in &mut call.arguments {
4140                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
4141                        labeled_arg.arg = start.clone();
4142                    }
4143                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
4144                        labeled_arg.arg = center.clone();
4145                    }
4146                }
4147                // Handle construction kwarg
4148                if let Some(construction_value) = construction {
4149                    let construction_exists = call
4150                        .arguments
4151                        .iter()
4152                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4153                    if *construction_value {
4154                        if construction_exists {
4155                            for labeled_arg in &mut call.arguments {
4156                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4157                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4158                                        value: ast::LiteralValue::Bool(true),
4159                                        raw: "true".to_string(),
4160                                        digest: None,
4161                                    })));
4162                                }
4163                            }
4164                        } else {
4165                            call.arguments.push(ast::LabeledArg {
4166                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4167                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4168                                    value: ast::LiteralValue::Bool(true),
4169                                    raw: "true".to_string(),
4170                                    digest: None,
4171                                }))),
4172                            });
4173                        }
4174                    } else {
4175                        call.arguments
4176                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4177                    }
4178                }
4179                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4180            }
4181        }
4182        AstMutateCommand::EditConstraintValue { value } => {
4183            if let NodeMut::BinaryExpression(binary_expr) = node {
4184                let left_is_constraint = matches!(
4185                    &binary_expr.left,
4186                    ast::BinaryPart::CallExpressionKw(call)
4187                        if matches!(
4188                            call.callee.name.name.as_str(),
4189                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
4190                        )
4191                );
4192                if left_is_constraint {
4193                    binary_expr.right = value.clone();
4194                } else {
4195                    binary_expr.left = value.clone();
4196                }
4197
4198                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4199            }
4200        }
4201        AstMutateCommand::EditCallUnlabeled { arg } => {
4202            if let NodeMut::CallExpressionKw(call) = node {
4203                call.unlabeled = Some(arg.clone());
4204                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4205            }
4206        }
4207        #[cfg(feature = "artifact-graph")]
4208        AstMutateCommand::EditVarInitialValue { value } => {
4209            if let NodeMut::NumericLiteral(numeric_literal) = node {
4210                // Update the initial value.
4211                let Ok(literal) = to_source_number(*value) else {
4212                    return TraversalReturn::new_break(Err(Error {
4213                        msg: format!("Could not convert number to AST literal: {:?}", *value),
4214                    }));
4215                };
4216                *numeric_literal = ast::Node::no_src(literal);
4217                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4218            }
4219        }
4220        AstMutateCommand::DeleteNode => {
4221            return TraversalReturn {
4222                mutate_body_item: MutateBodyItem::Delete,
4223                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
4224            };
4225        }
4226    }
4227    TraversalReturn::new_continue(())
4228}
4229
4230struct FindSketchBlockSourceRange {
4231    /// The source range of the sketch block before mutation.
4232    target_before_mutation: SourceRange,
4233    /// The source range of the sketch block's last body item after mutation. We
4234    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
4235    /// shared reference.
4236    found: Cell<Option<SourceRange>>,
4237}
4238
4239impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
4240    type Error = crate::front::Error;
4241
4242    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
4243        let Ok(node_range) = SourceRange::try_from(&node) else {
4244            return Ok(true);
4245        };
4246
4247        if let crate::walk::Node::SketchBlock(sketch_block) = node {
4248            if node_range.module_id() == self.target_before_mutation.module_id()
4249                && node_range.start() == self.target_before_mutation.start()
4250                // End shouldn't match since we added something.
4251                && node_range.end() >= self.target_before_mutation.end()
4252            {
4253                self.found.set(sketch_block.body.items.last().map(SourceRange::from));
4254                return Ok(false);
4255            } else {
4256                // We found a different sketch block. No need to descend into
4257                // its children since sketch blocks cannot be nested.
4258                return Ok(true);
4259            }
4260        }
4261
4262        for child in node.children().iter() {
4263            if !child.visit(*self)? {
4264                return Ok(false);
4265            }
4266        }
4267
4268        Ok(true)
4269    }
4270}
4271
4272/// After adding an item to a sketch block, find the sketch block, and get the
4273/// source range of the added item. We assume that the added item is the last
4274/// item in the sketch block and that the sketch block's source range has grown,
4275/// but not moved from its starting offset.
4276///
4277/// TODO: Do we need to format *before* mutation in case formatting moves the
4278/// sketch block forward?
4279fn find_sketch_block_added_item(
4280    ast: &ast::Node<ast::Program>,
4281    range_before_mutation: SourceRange,
4282) -> api::Result<SourceRange> {
4283    let find = FindSketchBlockSourceRange {
4284        target_before_mutation: range_before_mutation,
4285        found: Cell::new(None),
4286    };
4287    let node = crate::walk::Node::from(ast);
4288    node.visit(&find)?;
4289    find.found.into_inner().ok_or_else(|| api::Error {
4290        msg: format!("Source range after mutation not found for range before mutation: {range_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
4291    })
4292}
4293
4294fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
4295    // TODO: Don't duplicate this from lib.rs Program.
4296    ast.recast_top(&Default::default(), 0)
4297}
4298
4299pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
4300    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
4301        inner: ast::ArrayExpression {
4302            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
4303            non_code_meta: Default::default(),
4304            digest: None,
4305        },
4306        start: Default::default(),
4307        end: Default::default(),
4308        module_id: Default::default(),
4309        outer_attrs: Default::default(),
4310        pre_comments: Default::default(),
4311        comment_start: Default::default(),
4312    })))
4313}
4314
4315fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
4316    match expr {
4317        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
4318            inner: ast::Literal::from(to_source_number(*number)?),
4319            start: Default::default(),
4320            end: Default::default(),
4321            module_id: Default::default(),
4322            outer_attrs: Default::default(),
4323            pre_comments: Default::default(),
4324            comment_start: Default::default(),
4325        }))),
4326        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
4327            inner: ast::SketchVar {
4328                initial: Some(Box::new(ast::Node {
4329                    inner: to_source_number(*number)?,
4330                    start: Default::default(),
4331                    end: Default::default(),
4332                    module_id: Default::default(),
4333                    outer_attrs: Default::default(),
4334                    pre_comments: Default::default(),
4335                    comment_start: Default::default(),
4336                })),
4337                digest: None,
4338            },
4339            start: Default::default(),
4340            end: Default::default(),
4341            module_id: Default::default(),
4342            outer_attrs: Default::default(),
4343            pre_comments: Default::default(),
4344            comment_start: Default::default(),
4345        }))),
4346        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
4347    }
4348}
4349
4350fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
4351    Ok(ast::NumericLiteral {
4352        value: number.value,
4353        suffix: number.units,
4354        raw: format_number_literal(number.value, number.units, None)?,
4355        digest: None,
4356    })
4357}
4358
4359pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
4360    ast::Expr::Name(Box::new(ast_name(name)))
4361}
4362
4363fn ast_name(name: String) -> ast::Node<ast::Name> {
4364    ast::Node {
4365        inner: ast::Name {
4366            name: ast::Node {
4367                inner: ast::Identifier { name, digest: None },
4368                start: Default::default(),
4369                end: Default::default(),
4370                module_id: Default::default(),
4371                outer_attrs: Default::default(),
4372                pre_comments: Default::default(),
4373                comment_start: Default::default(),
4374            },
4375            path: Vec::new(),
4376            abs_path: false,
4377            digest: None,
4378        },
4379        start: Default::default(),
4380        end: Default::default(),
4381        module_id: Default::default(),
4382        outer_attrs: Default::default(),
4383        pre_comments: Default::default(),
4384        comment_start: Default::default(),
4385    }
4386}
4387
4388pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
4389    ast::Name {
4390        name: ast::Node {
4391            inner: ast::Identifier {
4392                name: name.to_owned(),
4393                digest: None,
4394            },
4395            start: Default::default(),
4396            end: Default::default(),
4397            module_id: Default::default(),
4398            outer_attrs: Default::default(),
4399            pre_comments: Default::default(),
4400            comment_start: Default::default(),
4401        },
4402        path: Default::default(),
4403        abs_path: false,
4404        digest: None,
4405    }
4406}
4407
4408// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
4409
4410/// Create an AST node for coincident([expr1, expr2])
4411pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
4412    // Create array [expr1, expr2]
4413    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4414        elements: vec![expr1, expr2],
4415        digest: None,
4416        non_code_meta: Default::default(),
4417    })));
4418
4419    // Create coincident([...])
4420    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4421        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
4422        unlabeled: Some(array_expr),
4423        arguments: Default::default(),
4424        digest: None,
4425        non_code_meta: Default::default(),
4426    })))
4427}
4428
4429/// Create an AST node for line(start = [...], end = [...])
4430pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
4431    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4432        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
4433        unlabeled: None,
4434        arguments: vec![
4435            ast::LabeledArg {
4436                label: Some(ast::Identifier::new(LINE_START_PARAM)),
4437                arg: start_ast,
4438            },
4439            ast::LabeledArg {
4440                label: Some(ast::Identifier::new(LINE_END_PARAM)),
4441                arg: end_ast,
4442            },
4443        ],
4444        digest: None,
4445        non_code_meta: Default::default(),
4446    })))
4447}
4448
4449/// Create an AST node for arc(start = [...], end = [...], center = [...])
4450pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
4451    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4452        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
4453        unlabeled: None,
4454        arguments: vec![
4455            ast::LabeledArg {
4456                label: Some(ast::Identifier::new(ARC_START_PARAM)),
4457                arg: start_ast,
4458            },
4459            ast::LabeledArg {
4460                label: Some(ast::Identifier::new(ARC_END_PARAM)),
4461                arg: end_ast,
4462            },
4463            ast::LabeledArg {
4464                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
4465                arg: center_ast,
4466            },
4467        ],
4468        digest: None,
4469        non_code_meta: Default::default(),
4470    })))
4471}
4472
4473/// Create an AST node for circle(start = [...], center = [...])
4474pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
4475    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4476        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
4477        unlabeled: None,
4478        arguments: vec![
4479            ast::LabeledArg {
4480                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
4481                arg: start_ast,
4482            },
4483            ast::LabeledArg {
4484                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
4485                arg: center_ast,
4486            },
4487        ],
4488        digest: None,
4489        non_code_meta: Default::default(),
4490    })))
4491}
4492
4493/// Create an AST node for horizontal(line)
4494pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
4495    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4496        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
4497        unlabeled: Some(line_expr),
4498        arguments: Default::default(),
4499        digest: None,
4500        non_code_meta: Default::default(),
4501    })))
4502}
4503
4504/// Create an AST node for vertical(line)
4505pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
4506    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4507        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
4508        unlabeled: Some(line_expr),
4509        arguments: Default::default(),
4510        digest: None,
4511        non_code_meta: Default::default(),
4512    })))
4513}
4514
4515/// Create a member expression like object.property (e.g., line1.end)
4516pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
4517    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
4518        object: object_expr,
4519        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
4520            name: ast::Node::no_src(ast::Identifier {
4521                name: property.to_string(),
4522                digest: None,
4523            }),
4524            path: Vec::new(),
4525            abs_path: false,
4526            digest: None,
4527        }))),
4528        computed: false,
4529        digest: None,
4530    })))
4531}
4532
4533/// Create an AST node for `fixed([point, [x, y]])`.
4534fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
4535    // Create [x, y] array literal.
4536    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
4537        position.x,
4538    )?))));
4539    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
4540        position.y,
4541    )?))));
4542    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4543        elements: vec![x_literal, y_literal],
4544        digest: None,
4545        non_code_meta: Default::default(),
4546    })));
4547
4548    // Create [point, [x, y]] outer array.
4549    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4550        elements: vec![point_expr, point_array],
4551        digest: None,
4552        non_code_meta: Default::default(),
4553    })));
4554
4555    // Create fixed([...])
4556    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
4557        ast::CallExpressionKw {
4558            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
4559            unlabeled: Some(array_expr),
4560            arguments: Default::default(),
4561            digest: None,
4562            non_code_meta: Default::default(),
4563        },
4564    ))))
4565}
4566
4567/// Create an AST node for equalLength([line1, line2, ...])
4568pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
4569    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4570        elements: line_exprs,
4571        digest: None,
4572        non_code_meta: Default::default(),
4573    })));
4574
4575    // Create equalLength([...])
4576    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4577        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
4578        unlabeled: Some(array_expr),
4579        arguments: Default::default(),
4580        digest: None,
4581        non_code_meta: Default::default(),
4582    })))
4583}
4584
4585/// Create an AST node for tangent([seg1, seg2])
4586pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
4587    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4588        elements: vec![seg1_expr, seg2_expr],
4589        digest: None,
4590        non_code_meta: Default::default(),
4591    })));
4592
4593    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4594        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
4595        unlabeled: Some(array_expr),
4596        arguments: Default::default(),
4597        digest: None,
4598        non_code_meta: Default::default(),
4599    })))
4600}
4601
4602#[cfg(all(feature = "artifact-graph", test))]
4603mod tests {
4604    use super::*;
4605    use crate::engine::PlaneName;
4606    use crate::front::Distance;
4607    use crate::front::Fixed;
4608    use crate::front::FixedPoint;
4609    use crate::front::Object;
4610    use crate::front::Plane;
4611    use crate::front::Sketch;
4612    use crate::front::Tangent;
4613    use crate::frontend::sketch::Vertical;
4614    use crate::pretty::NumericSuffix;
4615
4616    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
4617        for object in &scene_graph.objects {
4618            if let ObjectKind::Sketch(_) = &object.kind {
4619                return Some(object);
4620            }
4621        }
4622        None
4623    }
4624
4625    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
4626        for object in &scene_graph.objects {
4627            if let ObjectKind::Face(_) = &object.kind {
4628                return Some(object);
4629            }
4630        }
4631        None
4632    }
4633
4634    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
4635        for object in &scene_graph.objects {
4636            if matches!(&object.kind, ObjectKind::Wall(_)) {
4637                return Some(object.id);
4638            }
4639        }
4640        None
4641    }
4642
4643    #[test]
4644    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
4645        let source = "\
4646region001 = region(point = [0.1, 0.1], sketch = s)
4647extrude001 = extrude(region001, length = 5)
4648revolve001 = revolve(region001, axis = Y)
4649sweep001 = sweep(region001, path = path001)
4650loft001 = loft(region001)
4651not_sweep001 = shell(extrude001, faces = [], thickness = 1)
4652";
4653
4654        let program = Program::parse(source).unwrap().0.unwrap();
4655
4656        assert_eq!(
4657            region_name_from_sweep_variable(&program.ast, "extrude001"),
4658            Some("region001".to_owned())
4659        );
4660        assert_eq!(
4661            region_name_from_sweep_variable(&program.ast, "revolve001"),
4662            Some("region001".to_owned())
4663        );
4664        assert_eq!(
4665            region_name_from_sweep_variable(&program.ast, "sweep001"),
4666            Some("region001".to_owned())
4667        );
4668        assert_eq!(
4669            region_name_from_sweep_variable(&program.ast, "loft001"),
4670            Some("region001".to_owned())
4671        );
4672        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
4673    }
4674
4675    #[track_caller]
4676    fn expect_sketch(object: &Object) -> &Sketch {
4677        if let ObjectKind::Sketch(sketch) = &object.kind {
4678            sketch
4679        } else {
4680            panic!("Object is not a sketch: {:?}", object);
4681        }
4682    }
4683
4684    #[tokio::test(flavor = "multi_thread")]
4685    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
4686        let source = "\
4687@settings(experimentalFeatures = allow)
4688
4689sketch(on = XY) {
4690  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4691}
4692
4693bad = missing_name
4694";
4695        let program = Program::parse(source).unwrap().0.unwrap();
4696
4697        let mut frontend = FrontendState::new();
4698
4699        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4700        let mock_ctx = ExecutorContext::new_mock(None).await;
4701        let version = Version(0);
4702        let project_id = ProjectId(0);
4703        let file_id = FileId(0);
4704
4705        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
4706            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
4707        };
4708
4709        let sketch_id = frontend
4710            .scene_graph
4711            .objects
4712            .iter()
4713            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
4714            .expect("Expected sketch object from errored hack_set_program");
4715
4716        frontend
4717            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
4718            .await
4719            .unwrap();
4720
4721        ctx.close().await;
4722        mock_ctx.close().await;
4723    }
4724
4725    #[tokio::test(flavor = "multi_thread")]
4726    async fn test_new_sketch_add_point_edit_point() {
4727        let program = Program::empty();
4728
4729        let mut frontend = FrontendState::new();
4730        frontend.program = program;
4731
4732        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4733        let mock_ctx = ExecutorContext::new_mock(None).await;
4734        let version = Version(0);
4735
4736        let sketch_args = SketchCtor {
4737            on: Plane::Default(PlaneName::Xy),
4738        };
4739        let (_src_delta, scene_delta, sketch_id) = frontend
4740            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4741            .await
4742            .unwrap();
4743        assert_eq!(sketch_id, ObjectId(1));
4744        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4745        let sketch_object = &scene_delta.new_graph.objects[1];
4746        assert_eq!(sketch_object.id, ObjectId(1));
4747        assert_eq!(
4748            sketch_object.kind,
4749            ObjectKind::Sketch(Sketch {
4750                args: SketchCtor {
4751                    on: Plane::Default(PlaneName::Xy)
4752                },
4753                plane: ObjectId(0),
4754                segments: vec![],
4755                constraints: vec![],
4756            })
4757        );
4758        assert_eq!(scene_delta.new_graph.objects.len(), 2);
4759
4760        let point_ctor = PointCtor {
4761            position: Point2d {
4762                x: Expr::Number(Number {
4763                    value: 1.0,
4764                    units: NumericSuffix::Inch,
4765                }),
4766                y: Expr::Number(Number {
4767                    value: 2.0,
4768                    units: NumericSuffix::Inch,
4769                }),
4770            },
4771        };
4772        let segment = SegmentCtor::Point(point_ctor);
4773        let (src_delta, scene_delta) = frontend
4774            .add_segment(&mock_ctx, version, sketch_id, segment, None)
4775            .await
4776            .unwrap();
4777        assert_eq!(
4778            src_delta.text.as_str(),
4779            "@settings(experimentalFeatures = allow)
4780
4781sketch001 = sketch(on = XY) {
4782  point(at = [1in, 2in])
4783}
4784"
4785        );
4786        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
4787        assert_eq!(scene_delta.new_graph.objects.len(), 3);
4788        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4789            assert_eq!(scene_object.id.0, i);
4790        }
4791
4792        let point_id = *scene_delta.new_objects.last().unwrap();
4793
4794        let point_ctor = PointCtor {
4795            position: Point2d {
4796                x: Expr::Number(Number {
4797                    value: 3.0,
4798                    units: NumericSuffix::Inch,
4799                }),
4800                y: Expr::Number(Number {
4801                    value: 4.0,
4802                    units: NumericSuffix::Inch,
4803                }),
4804            },
4805        };
4806        let segments = vec![ExistingSegmentCtor {
4807            id: point_id,
4808            ctor: SegmentCtor::Point(point_ctor),
4809        }];
4810        let (src_delta, scene_delta) = frontend
4811            .edit_segments(&mock_ctx, version, sketch_id, segments)
4812            .await
4813            .unwrap();
4814        assert_eq!(
4815            src_delta.text.as_str(),
4816            "@settings(experimentalFeatures = allow)
4817
4818sketch001 = sketch(on = XY) {
4819  point(at = [3in, 4in])
4820}
4821"
4822        );
4823        assert_eq!(scene_delta.new_objects, vec![]);
4824        assert_eq!(scene_delta.new_graph.objects.len(), 3);
4825
4826        ctx.close().await;
4827        mock_ctx.close().await;
4828    }
4829
4830    #[tokio::test(flavor = "multi_thread")]
4831    async fn test_new_sketch_add_line_edit_line() {
4832        let program = Program::empty();
4833
4834        let mut frontend = FrontendState::new();
4835        frontend.program = program;
4836
4837        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4838        let mock_ctx = ExecutorContext::new_mock(None).await;
4839        let version = Version(0);
4840
4841        let sketch_args = SketchCtor {
4842            on: Plane::Default(PlaneName::Xy),
4843        };
4844        let (_src_delta, scene_delta, sketch_id) = frontend
4845            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4846            .await
4847            .unwrap();
4848        assert_eq!(sketch_id, ObjectId(1));
4849        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4850        let sketch_object = &scene_delta.new_graph.objects[1];
4851        assert_eq!(sketch_object.id, ObjectId(1));
4852        assert_eq!(
4853            sketch_object.kind,
4854            ObjectKind::Sketch(Sketch {
4855                args: SketchCtor {
4856                    on: Plane::Default(PlaneName::Xy)
4857                },
4858                plane: ObjectId(0),
4859                segments: vec![],
4860                constraints: vec![],
4861            })
4862        );
4863        assert_eq!(scene_delta.new_graph.objects.len(), 2);
4864
4865        let line_ctor = LineCtor {
4866            start: Point2d {
4867                x: Expr::Number(Number {
4868                    value: 0.0,
4869                    units: NumericSuffix::Mm,
4870                }),
4871                y: Expr::Number(Number {
4872                    value: 0.0,
4873                    units: NumericSuffix::Mm,
4874                }),
4875            },
4876            end: Point2d {
4877                x: Expr::Number(Number {
4878                    value: 10.0,
4879                    units: NumericSuffix::Mm,
4880                }),
4881                y: Expr::Number(Number {
4882                    value: 10.0,
4883                    units: NumericSuffix::Mm,
4884                }),
4885            },
4886            construction: None,
4887        };
4888        let segment = SegmentCtor::Line(line_ctor);
4889        let (src_delta, scene_delta) = frontend
4890            .add_segment(&mock_ctx, version, sketch_id, segment, None)
4891            .await
4892            .unwrap();
4893        assert_eq!(
4894            src_delta.text.as_str(),
4895            "@settings(experimentalFeatures = allow)
4896
4897sketch001 = sketch(on = XY) {
4898  line(start = [0mm, 0mm], end = [10mm, 10mm])
4899}
4900"
4901        );
4902        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
4903        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4904        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4905            assert_eq!(scene_object.id.0, i);
4906        }
4907
4908        // The new objects are the end points and then the line.
4909        let line = *scene_delta.new_objects.last().unwrap();
4910
4911        let line_ctor = LineCtor {
4912            start: Point2d {
4913                x: Expr::Number(Number {
4914                    value: 1.0,
4915                    units: NumericSuffix::Mm,
4916                }),
4917                y: Expr::Number(Number {
4918                    value: 2.0,
4919                    units: NumericSuffix::Mm,
4920                }),
4921            },
4922            end: Point2d {
4923                x: Expr::Number(Number {
4924                    value: 13.0,
4925                    units: NumericSuffix::Mm,
4926                }),
4927                y: Expr::Number(Number {
4928                    value: 14.0,
4929                    units: NumericSuffix::Mm,
4930                }),
4931            },
4932            construction: None,
4933        };
4934        let segments = vec![ExistingSegmentCtor {
4935            id: line,
4936            ctor: SegmentCtor::Line(line_ctor),
4937        }];
4938        let (src_delta, scene_delta) = frontend
4939            .edit_segments(&mock_ctx, version, sketch_id, segments)
4940            .await
4941            .unwrap();
4942        assert_eq!(
4943            src_delta.text.as_str(),
4944            "@settings(experimentalFeatures = allow)
4945
4946sketch001 = sketch(on = XY) {
4947  line(start = [1mm, 2mm], end = [13mm, 14mm])
4948}
4949"
4950        );
4951        assert_eq!(scene_delta.new_objects, vec![]);
4952        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4953
4954        ctx.close().await;
4955        mock_ctx.close().await;
4956    }
4957
4958    #[tokio::test(flavor = "multi_thread")]
4959    async fn test_new_sketch_add_arc_edit_arc() {
4960        let program = Program::empty();
4961
4962        let mut frontend = FrontendState::new();
4963        frontend.program = program;
4964
4965        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4966        let mock_ctx = ExecutorContext::new_mock(None).await;
4967        let version = Version(0);
4968
4969        let sketch_args = SketchCtor {
4970            on: Plane::Default(PlaneName::Xy),
4971        };
4972        let (_src_delta, scene_delta, sketch_id) = frontend
4973            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4974            .await
4975            .unwrap();
4976        assert_eq!(sketch_id, ObjectId(1));
4977        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4978        let sketch_object = &scene_delta.new_graph.objects[1];
4979        assert_eq!(sketch_object.id, ObjectId(1));
4980        assert_eq!(
4981            sketch_object.kind,
4982            ObjectKind::Sketch(Sketch {
4983                args: SketchCtor {
4984                    on: Plane::Default(PlaneName::Xy),
4985                },
4986                plane: ObjectId(0),
4987                segments: vec![],
4988                constraints: vec![],
4989            })
4990        );
4991        assert_eq!(scene_delta.new_graph.objects.len(), 2);
4992
4993        let arc_ctor = ArcCtor {
4994            start: Point2d {
4995                x: Expr::Var(Number {
4996                    value: 0.0,
4997                    units: NumericSuffix::Mm,
4998                }),
4999                y: Expr::Var(Number {
5000                    value: 0.0,
5001                    units: NumericSuffix::Mm,
5002                }),
5003            },
5004            end: Point2d {
5005                x: Expr::Var(Number {
5006                    value: 10.0,
5007                    units: NumericSuffix::Mm,
5008                }),
5009                y: Expr::Var(Number {
5010                    value: 10.0,
5011                    units: NumericSuffix::Mm,
5012                }),
5013            },
5014            center: Point2d {
5015                x: Expr::Var(Number {
5016                    value: 10.0,
5017                    units: NumericSuffix::Mm,
5018                }),
5019                y: Expr::Var(Number {
5020                    value: 0.0,
5021                    units: NumericSuffix::Mm,
5022                }),
5023            },
5024            construction: None,
5025        };
5026        let segment = SegmentCtor::Arc(arc_ctor);
5027        let (src_delta, scene_delta) = frontend
5028            .add_segment(&mock_ctx, version, sketch_id, segment, None)
5029            .await
5030            .unwrap();
5031        assert_eq!(
5032            src_delta.text.as_str(),
5033            "@settings(experimentalFeatures = allow)
5034
5035sketch001 = sketch(on = XY) {
5036  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
5037}
5038"
5039        );
5040        assert_eq!(
5041            scene_delta.new_objects,
5042            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
5043        );
5044        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
5045            assert_eq!(scene_object.id.0, i);
5046        }
5047        assert_eq!(scene_delta.new_graph.objects.len(), 6);
5048
5049        // The new objects are the end points, the center, and then the arc.
5050        let arc = *scene_delta.new_objects.last().unwrap();
5051
5052        let arc_ctor = ArcCtor {
5053            start: Point2d {
5054                x: Expr::Var(Number {
5055                    value: 1.0,
5056                    units: NumericSuffix::Mm,
5057                }),
5058                y: Expr::Var(Number {
5059                    value: 2.0,
5060                    units: NumericSuffix::Mm,
5061                }),
5062            },
5063            end: Point2d {
5064                x: Expr::Var(Number {
5065                    value: 13.0,
5066                    units: NumericSuffix::Mm,
5067                }),
5068                y: Expr::Var(Number {
5069                    value: 14.0,
5070                    units: NumericSuffix::Mm,
5071                }),
5072            },
5073            center: Point2d {
5074                x: Expr::Var(Number {
5075                    value: 13.0,
5076                    units: NumericSuffix::Mm,
5077                }),
5078                y: Expr::Var(Number {
5079                    value: 2.0,
5080                    units: NumericSuffix::Mm,
5081                }),
5082            },
5083            construction: None,
5084        };
5085        let segments = vec![ExistingSegmentCtor {
5086            id: arc,
5087            ctor: SegmentCtor::Arc(arc_ctor),
5088        }];
5089        let (src_delta, scene_delta) = frontend
5090            .edit_segments(&mock_ctx, version, sketch_id, segments)
5091            .await
5092            .unwrap();
5093        assert_eq!(
5094            src_delta.text.as_str(),
5095            "@settings(experimentalFeatures = allow)
5096
5097sketch001 = sketch(on = XY) {
5098  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
5099}
5100"
5101        );
5102        assert_eq!(scene_delta.new_objects, vec![]);
5103        assert_eq!(scene_delta.new_graph.objects.len(), 6);
5104
5105        ctx.close().await;
5106        mock_ctx.close().await;
5107    }
5108
5109    #[tokio::test(flavor = "multi_thread")]
5110    async fn test_new_sketch_add_circle_edit_circle() {
5111        let program = Program::empty();
5112
5113        let mut frontend = FrontendState::new();
5114        frontend.program = program;
5115
5116        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5117        let mock_ctx = ExecutorContext::new_mock(None).await;
5118        let version = Version(0);
5119
5120        let sketch_args = SketchCtor {
5121            on: Plane::Default(PlaneName::Xy),
5122        };
5123        let (_src_delta, _scene_delta, sketch_id) = frontend
5124            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5125            .await
5126            .unwrap();
5127
5128        // Add a circle segment.
5129        let circle_ctor = CircleCtor {
5130            start: Point2d {
5131                x: Expr::Var(Number {
5132                    value: 5.0,
5133                    units: NumericSuffix::Mm,
5134                }),
5135                y: Expr::Var(Number {
5136                    value: 0.0,
5137                    units: NumericSuffix::Mm,
5138                }),
5139            },
5140            center: Point2d {
5141                x: Expr::Var(Number {
5142                    value: 0.0,
5143                    units: NumericSuffix::Mm,
5144                }),
5145                y: Expr::Var(Number {
5146                    value: 0.0,
5147                    units: NumericSuffix::Mm,
5148                }),
5149            },
5150            construction: None,
5151        };
5152        let segment = SegmentCtor::Circle(circle_ctor);
5153        let (src_delta, scene_delta) = frontend
5154            .add_segment(&mock_ctx, version, sketch_id, segment, None)
5155            .await
5156            .unwrap();
5157        assert_eq!(
5158            src_delta.text.as_str(),
5159            "@settings(experimentalFeatures = allow)
5160
5161sketch001 = sketch(on = XY) {
5162  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5163}
5164"
5165        );
5166        // The new objects are start, center, and then the circle segment.
5167        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
5168        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5169
5170        let circle = *scene_delta.new_objects.last().unwrap();
5171
5172        // Edit the circle segment.
5173        let circle_ctor = CircleCtor {
5174            start: Point2d {
5175                x: Expr::Var(Number {
5176                    value: 10.0,
5177                    units: NumericSuffix::Mm,
5178                }),
5179                y: Expr::Var(Number {
5180                    value: 0.0,
5181                    units: NumericSuffix::Mm,
5182                }),
5183            },
5184            center: Point2d {
5185                x: Expr::Var(Number {
5186                    value: 3.0,
5187                    units: NumericSuffix::Mm,
5188                }),
5189                y: Expr::Var(Number {
5190                    value: 4.0,
5191                    units: NumericSuffix::Mm,
5192                }),
5193            },
5194            construction: None,
5195        };
5196        let segments = vec![ExistingSegmentCtor {
5197            id: circle,
5198            ctor: SegmentCtor::Circle(circle_ctor),
5199        }];
5200        let (src_delta, scene_delta) = frontend
5201            .edit_segments(&mock_ctx, version, sketch_id, segments)
5202            .await
5203            .unwrap();
5204        assert_eq!(
5205            src_delta.text.as_str(),
5206            "@settings(experimentalFeatures = allow)
5207
5208sketch001 = sketch(on = XY) {
5209  circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
5210}
5211"
5212        );
5213        assert_eq!(scene_delta.new_objects, vec![]);
5214        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5215
5216        ctx.close().await;
5217        mock_ctx.close().await;
5218    }
5219
5220    #[tokio::test(flavor = "multi_thread")]
5221    async fn test_delete_circle() {
5222        let initial_source = "@settings(experimentalFeatures = allow)
5223
5224sketch001 = sketch(on = XY) {
5225  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5226}
5227";
5228
5229        let program = Program::parse(initial_source).unwrap().0.unwrap();
5230        let mut frontend = FrontendState::new();
5231
5232        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5233        let mock_ctx = ExecutorContext::new_mock(None).await;
5234        let version = Version(0);
5235
5236        frontend.hack_set_program(&ctx, program).await.unwrap();
5237        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5238        let sketch_id = sketch_object.id;
5239        let sketch = expect_sketch(sketch_object);
5240
5241        // The sketch should have 3 segments: start point, center point, and the circle.
5242        assert_eq!(sketch.segments.len(), 3);
5243        let circle_id = sketch.segments[2];
5244
5245        // Delete the circle.
5246        let (src_delta, scene_delta) = frontend
5247            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
5248            .await
5249            .unwrap();
5250        assert_eq!(
5251            src_delta.text.as_str(),
5252            "@settings(experimentalFeatures = allow)
5253
5254sketch001 = sketch(on = XY) {
5255}
5256"
5257        );
5258        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
5259        let new_sketch = expect_sketch(new_sketch_object);
5260        assert_eq!(new_sketch.segments.len(), 0);
5261
5262        ctx.close().await;
5263        mock_ctx.close().await;
5264    }
5265
5266    #[tokio::test(flavor = "multi_thread")]
5267    async fn test_edit_circle_via_point() {
5268        let initial_source = "@settings(experimentalFeatures = allow)
5269
5270sketch001 = sketch(on = XY) {
5271  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5272}
5273";
5274
5275        let program = Program::parse(initial_source).unwrap().0.unwrap();
5276        let mut frontend = FrontendState::new();
5277
5278        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5279        let mock_ctx = ExecutorContext::new_mock(None).await;
5280        let version = Version(0);
5281
5282        frontend.hack_set_program(&ctx, program).await.unwrap();
5283        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5284        let sketch_id = sketch_object.id;
5285        let sketch = expect_sketch(sketch_object);
5286
5287        // Find the circle segment and its start point.
5288        let circle_id = sketch
5289            .segments
5290            .iter()
5291            .copied()
5292            .find(|seg_id| {
5293                matches!(
5294                    &frontend.scene_graph.objects[seg_id.0].kind,
5295                    ObjectKind::Segment {
5296                        segment: Segment::Circle(_)
5297                    }
5298                )
5299            })
5300            .expect("Expected a circle segment in sketch");
5301        let circle_object = &frontend.scene_graph.objects[circle_id.0];
5302        let ObjectKind::Segment {
5303            segment: Segment::Circle(circle),
5304        } = &circle_object.kind
5305        else {
5306            panic!("Expected circle segment, got: {:?}", circle_object.kind);
5307        };
5308        let start_point_id = circle.start;
5309
5310        // Edit the start point via SegmentCtor::Point.
5311        let segments = vec![ExistingSegmentCtor {
5312            id: start_point_id,
5313            ctor: SegmentCtor::Point(PointCtor {
5314                position: Point2d {
5315                    x: Expr::Var(Number {
5316                        value: 7.0,
5317                        units: NumericSuffix::Mm,
5318                    }),
5319                    y: Expr::Var(Number {
5320                        value: 1.0,
5321                        units: NumericSuffix::Mm,
5322                    }),
5323                },
5324            }),
5325        }];
5326        let (src_delta, _scene_delta) = frontend
5327            .edit_segments(&mock_ctx, version, sketch_id, segments)
5328            .await
5329            .unwrap();
5330        assert_eq!(
5331            src_delta.text.as_str(),
5332            "@settings(experimentalFeatures = allow)
5333
5334sketch001 = sketch(on = XY) {
5335  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
5336}
5337"
5338        );
5339
5340        ctx.close().await;
5341        mock_ctx.close().await;
5342    }
5343
5344    #[tokio::test(flavor = "multi_thread")]
5345    async fn test_add_line_when_sketch_block_uses_variable() {
5346        let initial_source = "@settings(experimentalFeatures = allow)
5347
5348s = sketch(on = XY) {}
5349";
5350
5351        let program = Program::parse(initial_source).unwrap().0.unwrap();
5352
5353        let mut frontend = FrontendState::new();
5354
5355        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5356        let mock_ctx = ExecutorContext::new_mock(None).await;
5357        let version = Version(0);
5358
5359        frontend.hack_set_program(&ctx, program).await.unwrap();
5360        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5361        let sketch_id = sketch_object.id;
5362
5363        let line_ctor = LineCtor {
5364            start: Point2d {
5365                x: Expr::Number(Number {
5366                    value: 0.0,
5367                    units: NumericSuffix::Mm,
5368                }),
5369                y: Expr::Number(Number {
5370                    value: 0.0,
5371                    units: NumericSuffix::Mm,
5372                }),
5373            },
5374            end: Point2d {
5375                x: Expr::Number(Number {
5376                    value: 10.0,
5377                    units: NumericSuffix::Mm,
5378                }),
5379                y: Expr::Number(Number {
5380                    value: 10.0,
5381                    units: NumericSuffix::Mm,
5382                }),
5383            },
5384            construction: None,
5385        };
5386        let segment = SegmentCtor::Line(line_ctor);
5387        let (src_delta, scene_delta) = frontend
5388            .add_segment(&mock_ctx, version, sketch_id, segment, None)
5389            .await
5390            .unwrap();
5391        assert_eq!(
5392            src_delta.text.as_str(),
5393            "@settings(experimentalFeatures = allow)
5394
5395s = sketch(on = XY) {
5396  line(start = [0mm, 0mm], end = [10mm, 10mm])
5397}
5398"
5399        );
5400        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
5401        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5402
5403        ctx.close().await;
5404        mock_ctx.close().await;
5405    }
5406
5407    #[tokio::test(flavor = "multi_thread")]
5408    async fn test_new_sketch_add_line_delete_sketch() {
5409        let program = Program::empty();
5410
5411        let mut frontend = FrontendState::new();
5412        frontend.program = program;
5413
5414        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5415        let mock_ctx = ExecutorContext::new_mock(None).await;
5416        let version = Version(0);
5417
5418        let sketch_args = SketchCtor {
5419            on: Plane::Default(PlaneName::Xy),
5420        };
5421        let (_src_delta, scene_delta, sketch_id) = frontend
5422            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5423            .await
5424            .unwrap();
5425        assert_eq!(sketch_id, ObjectId(1));
5426        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
5427        let sketch_object = &scene_delta.new_graph.objects[1];
5428        assert_eq!(sketch_object.id, ObjectId(1));
5429        assert_eq!(
5430            sketch_object.kind,
5431            ObjectKind::Sketch(Sketch {
5432                args: SketchCtor {
5433                    on: Plane::Default(PlaneName::Xy)
5434                },
5435                plane: ObjectId(0),
5436                segments: vec![],
5437                constraints: vec![],
5438            })
5439        );
5440        assert_eq!(scene_delta.new_graph.objects.len(), 2);
5441
5442        let line_ctor = LineCtor {
5443            start: Point2d {
5444                x: Expr::Number(Number {
5445                    value: 0.0,
5446                    units: NumericSuffix::Mm,
5447                }),
5448                y: Expr::Number(Number {
5449                    value: 0.0,
5450                    units: NumericSuffix::Mm,
5451                }),
5452            },
5453            end: Point2d {
5454                x: Expr::Number(Number {
5455                    value: 10.0,
5456                    units: NumericSuffix::Mm,
5457                }),
5458                y: Expr::Number(Number {
5459                    value: 10.0,
5460                    units: NumericSuffix::Mm,
5461                }),
5462            },
5463            construction: None,
5464        };
5465        let segment = SegmentCtor::Line(line_ctor);
5466        let (src_delta, scene_delta) = frontend
5467            .add_segment(&mock_ctx, version, sketch_id, segment, None)
5468            .await
5469            .unwrap();
5470        assert_eq!(
5471            src_delta.text.as_str(),
5472            "@settings(experimentalFeatures = allow)
5473
5474sketch001 = sketch(on = XY) {
5475  line(start = [0mm, 0mm], end = [10mm, 10mm])
5476}
5477"
5478        );
5479        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5480
5481        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
5482        assert_eq!(
5483            src_delta.text.as_str(),
5484            "@settings(experimentalFeatures = allow)
5485"
5486        );
5487        assert_eq!(scene_delta.new_graph.objects.len(), 0);
5488
5489        ctx.close().await;
5490        mock_ctx.close().await;
5491    }
5492
5493    #[tokio::test(flavor = "multi_thread")]
5494    async fn test_delete_sketch_when_sketch_block_uses_variable() {
5495        let initial_source = "@settings(experimentalFeatures = allow)
5496
5497s = sketch(on = XY) {}
5498";
5499
5500        let program = Program::parse(initial_source).unwrap().0.unwrap();
5501
5502        let mut frontend = FrontendState::new();
5503
5504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5505        let mock_ctx = ExecutorContext::new_mock(None).await;
5506        let version = Version(0);
5507
5508        frontend.hack_set_program(&ctx, program).await.unwrap();
5509        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5510        let sketch_id = sketch_object.id;
5511
5512        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
5513        assert_eq!(
5514            src_delta.text.as_str(),
5515            "@settings(experimentalFeatures = allow)
5516"
5517        );
5518        assert_eq!(scene_delta.new_graph.objects.len(), 0);
5519
5520        ctx.close().await;
5521        mock_ctx.close().await;
5522    }
5523
5524    #[tokio::test(flavor = "multi_thread")]
5525    async fn test_edit_line_when_editing_its_start_point() {
5526        let initial_source = "\
5527@settings(experimentalFeatures = allow)
5528
5529sketch(on = XY) {
5530  line(start = [var 1, var 2], end = [var 3, var 4])
5531}
5532";
5533
5534        let program = Program::parse(initial_source).unwrap().0.unwrap();
5535
5536        let mut frontend = FrontendState::new();
5537
5538        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5539        let mock_ctx = ExecutorContext::new_mock(None).await;
5540        let version = Version(0);
5541
5542        frontend.hack_set_program(&ctx, program).await.unwrap();
5543        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5544        let sketch_id = sketch_object.id;
5545        let sketch = expect_sketch(sketch_object);
5546
5547        let point_id = *sketch.segments.first().unwrap();
5548
5549        let point_ctor = PointCtor {
5550            position: Point2d {
5551                x: Expr::Var(Number {
5552                    value: 5.0,
5553                    units: NumericSuffix::Inch,
5554                }),
5555                y: Expr::Var(Number {
5556                    value: 6.0,
5557                    units: NumericSuffix::Inch,
5558                }),
5559            },
5560        };
5561        let segments = vec![ExistingSegmentCtor {
5562            id: point_id,
5563            ctor: SegmentCtor::Point(point_ctor),
5564        }];
5565        let (src_delta, scene_delta) = frontend
5566            .edit_segments(&mock_ctx, version, sketch_id, segments)
5567            .await
5568            .unwrap();
5569        assert_eq!(
5570            src_delta.text.as_str(),
5571            "\
5572@settings(experimentalFeatures = allow)
5573
5574sketch(on = XY) {
5575  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
5576}
5577"
5578        );
5579        assert_eq!(scene_delta.new_objects, vec![]);
5580        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5581
5582        ctx.close().await;
5583        mock_ctx.close().await;
5584    }
5585
5586    #[tokio::test(flavor = "multi_thread")]
5587    async fn test_edit_line_when_editing_its_end_point() {
5588        let initial_source = "\
5589@settings(experimentalFeatures = allow)
5590
5591sketch(on = XY) {
5592  line(start = [var 1, var 2], end = [var 3, var 4])
5593}
5594";
5595
5596        let program = Program::parse(initial_source).unwrap().0.unwrap();
5597
5598        let mut frontend = FrontendState::new();
5599
5600        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5601        let mock_ctx = ExecutorContext::new_mock(None).await;
5602        let version = Version(0);
5603
5604        frontend.hack_set_program(&ctx, program).await.unwrap();
5605        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5606        let sketch_id = sketch_object.id;
5607        let sketch = expect_sketch(sketch_object);
5608        let point_id = *sketch.segments.get(1).unwrap();
5609
5610        let point_ctor = PointCtor {
5611            position: Point2d {
5612                x: Expr::Var(Number {
5613                    value: 5.0,
5614                    units: NumericSuffix::Inch,
5615                }),
5616                y: Expr::Var(Number {
5617                    value: 6.0,
5618                    units: NumericSuffix::Inch,
5619                }),
5620            },
5621        };
5622        let segments = vec![ExistingSegmentCtor {
5623            id: point_id,
5624            ctor: SegmentCtor::Point(point_ctor),
5625        }];
5626        let (src_delta, scene_delta) = frontend
5627            .edit_segments(&mock_ctx, version, sketch_id, segments)
5628            .await
5629            .unwrap();
5630        assert_eq!(
5631            src_delta.text.as_str(),
5632            "\
5633@settings(experimentalFeatures = allow)
5634
5635sketch(on = XY) {
5636  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
5637}
5638"
5639        );
5640        assert_eq!(scene_delta.new_objects, vec![]);
5641        assert_eq!(
5642            scene_delta.new_graph.objects.len(),
5643            5,
5644            "{:#?}",
5645            scene_delta.new_graph.objects
5646        );
5647
5648        ctx.close().await;
5649        mock_ctx.close().await;
5650    }
5651
5652    #[tokio::test(flavor = "multi_thread")]
5653    async fn test_edit_line_with_coincident_feedback() {
5654        let initial_source = "\
5655@settings(experimentalFeatures = allow)
5656
5657sketch(on = XY) {
5658  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
5659  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5660  fixed([line1.start, [0, 0]])
5661  coincident([line1.end, line2.start])
5662  equalLength([line1, line2])
5663}
5664";
5665
5666        let program = Program::parse(initial_source).unwrap().0.unwrap();
5667
5668        let mut frontend = FrontendState::new();
5669
5670        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5671        let mock_ctx = ExecutorContext::new_mock(None).await;
5672        let version = Version(0);
5673
5674        frontend.hack_set_program(&ctx, program).await.unwrap();
5675        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5676        let sketch_id = sketch_object.id;
5677        let sketch = expect_sketch(sketch_object);
5678        let line2_end_id = *sketch.segments.get(4).unwrap();
5679
5680        let segments = vec![ExistingSegmentCtor {
5681            id: line2_end_id,
5682            ctor: SegmentCtor::Point(PointCtor {
5683                position: Point2d {
5684                    x: Expr::Var(Number {
5685                        value: 9.0,
5686                        units: NumericSuffix::None,
5687                    }),
5688                    y: Expr::Var(Number {
5689                        value: 10.0,
5690                        units: NumericSuffix::None,
5691                    }),
5692                },
5693            }),
5694        }];
5695        let (src_delta, scene_delta) = frontend
5696            .edit_segments(&mock_ctx, version, sketch_id, segments)
5697            .await
5698            .unwrap();
5699        assert_eq!(
5700            src_delta.text.as_str(),
5701            "\
5702@settings(experimentalFeatures = allow)
5703
5704sketch(on = XY) {
5705  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
5706  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
5707  fixed([line1.start, [0, 0]])
5708  coincident([line1.end, line2.start])
5709  equalLength([line1, line2])
5710}
5711"
5712        );
5713        assert_eq!(
5714            scene_delta.new_graph.objects.len(),
5715            11,
5716            "{:#?}",
5717            scene_delta.new_graph.objects
5718        );
5719
5720        ctx.close().await;
5721        mock_ctx.close().await;
5722    }
5723
5724    #[tokio::test(flavor = "multi_thread")]
5725    async fn test_delete_point_without_var() {
5726        let initial_source = "\
5727@settings(experimentalFeatures = allow)
5728
5729sketch(on = XY) {
5730  point(at = [var 1, var 2])
5731  point(at = [var 3, var 4])
5732  point(at = [var 5, var 6])
5733}
5734";
5735
5736        let program = Program::parse(initial_source).unwrap().0.unwrap();
5737
5738        let mut frontend = FrontendState::new();
5739
5740        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5741        let mock_ctx = ExecutorContext::new_mock(None).await;
5742        let version = Version(0);
5743
5744        frontend.hack_set_program(&ctx, program).await.unwrap();
5745        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5746        let sketch_id = sketch_object.id;
5747        let sketch = expect_sketch(sketch_object);
5748
5749        let point_id = *sketch.segments.get(1).unwrap();
5750
5751        let (src_delta, scene_delta) = frontend
5752            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
5753            .await
5754            .unwrap();
5755        assert_eq!(
5756            src_delta.text.as_str(),
5757            "\
5758@settings(experimentalFeatures = allow)
5759
5760sketch(on = XY) {
5761  point(at = [var 1mm, var 2mm])
5762  point(at = [var 5mm, var 6mm])
5763}
5764"
5765        );
5766        assert_eq!(scene_delta.new_objects, vec![]);
5767        assert_eq!(scene_delta.new_graph.objects.len(), 4);
5768
5769        ctx.close().await;
5770        mock_ctx.close().await;
5771    }
5772
5773    #[tokio::test(flavor = "multi_thread")]
5774    async fn test_delete_point_with_var() {
5775        let initial_source = "\
5776@settings(experimentalFeatures = allow)
5777
5778sketch(on = XY) {
5779  point(at = [var 1, var 2])
5780  point1 = point(at = [var 3, var 4])
5781  point(at = [var 5, var 6])
5782}
5783";
5784
5785        let program = Program::parse(initial_source).unwrap().0.unwrap();
5786
5787        let mut frontend = FrontendState::new();
5788
5789        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5790        let mock_ctx = ExecutorContext::new_mock(None).await;
5791        let version = Version(0);
5792
5793        frontend.hack_set_program(&ctx, program).await.unwrap();
5794        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5795        let sketch_id = sketch_object.id;
5796        let sketch = expect_sketch(sketch_object);
5797
5798        let point_id = *sketch.segments.get(1).unwrap();
5799
5800        let (src_delta, scene_delta) = frontend
5801            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
5802            .await
5803            .unwrap();
5804        assert_eq!(
5805            src_delta.text.as_str(),
5806            "\
5807@settings(experimentalFeatures = allow)
5808
5809sketch(on = XY) {
5810  point(at = [var 1mm, var 2mm])
5811  point(at = [var 5mm, var 6mm])
5812}
5813"
5814        );
5815        assert_eq!(scene_delta.new_objects, vec![]);
5816        assert_eq!(scene_delta.new_graph.objects.len(), 4);
5817
5818        ctx.close().await;
5819        mock_ctx.close().await;
5820    }
5821
5822    #[tokio::test(flavor = "multi_thread")]
5823    async fn test_delete_multiple_points() {
5824        let initial_source = "\
5825@settings(experimentalFeatures = allow)
5826
5827sketch(on = XY) {
5828  point(at = [var 1, var 2])
5829  point1 = point(at = [var 3, var 4])
5830  point(at = [var 5, var 6])
5831}
5832";
5833
5834        let program = Program::parse(initial_source).unwrap().0.unwrap();
5835
5836        let mut frontend = FrontendState::new();
5837
5838        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5839        let mock_ctx = ExecutorContext::new_mock(None).await;
5840        let version = Version(0);
5841
5842        frontend.hack_set_program(&ctx, program).await.unwrap();
5843        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5844        let sketch_id = sketch_object.id;
5845
5846        let sketch = expect_sketch(sketch_object);
5847
5848        let point1_id = *sketch.segments.first().unwrap();
5849        let point2_id = *sketch.segments.get(1).unwrap();
5850
5851        let (src_delta, scene_delta) = frontend
5852            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
5853            .await
5854            .unwrap();
5855        assert_eq!(
5856            src_delta.text.as_str(),
5857            "\
5858@settings(experimentalFeatures = allow)
5859
5860sketch(on = XY) {
5861  point(at = [var 5mm, var 6mm])
5862}
5863"
5864        );
5865        assert_eq!(scene_delta.new_objects, vec![]);
5866        assert_eq!(scene_delta.new_graph.objects.len(), 3);
5867
5868        ctx.close().await;
5869        mock_ctx.close().await;
5870    }
5871
5872    #[tokio::test(flavor = "multi_thread")]
5873    async fn test_delete_coincident_constraint() {
5874        let initial_source = "\
5875@settings(experimentalFeatures = allow)
5876
5877sketch(on = XY) {
5878  point1 = point(at = [var 1, var 2])
5879  point2 = point(at = [var 3, var 4])
5880  coincident([point1, point2])
5881  point(at = [var 5, var 6])
5882}
5883";
5884
5885        let program = Program::parse(initial_source).unwrap().0.unwrap();
5886
5887        let mut frontend = FrontendState::new();
5888
5889        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5890        let mock_ctx = ExecutorContext::new_mock(None).await;
5891        let version = Version(0);
5892
5893        frontend.hack_set_program(&ctx, program).await.unwrap();
5894        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5895        let sketch_id = sketch_object.id;
5896        let sketch = expect_sketch(sketch_object);
5897
5898        let coincident_id = *sketch.constraints.first().unwrap();
5899
5900        let (src_delta, scene_delta) = frontend
5901            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
5902            .await
5903            .unwrap();
5904        assert_eq!(
5905            src_delta.text.as_str(),
5906            "\
5907@settings(experimentalFeatures = allow)
5908
5909sketch(on = XY) {
5910  point1 = point(at = [var 1mm, var 2mm])
5911  point2 = point(at = [var 3mm, var 4mm])
5912  point(at = [var 5mm, var 6mm])
5913}
5914"
5915        );
5916        assert_eq!(scene_delta.new_objects, vec![]);
5917        assert_eq!(scene_delta.new_graph.objects.len(), 5);
5918
5919        ctx.close().await;
5920        mock_ctx.close().await;
5921    }
5922
5923    #[tokio::test(flavor = "multi_thread")]
5924    async fn test_delete_line_cascades_to_coincident_constraint() {
5925        let initial_source = "\
5926@settings(experimentalFeatures = allow)
5927
5928sketch(on = XY) {
5929  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5930  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5931  coincident([line1.end, line2.start])
5932}
5933";
5934
5935        let program = Program::parse(initial_source).unwrap().0.unwrap();
5936
5937        let mut frontend = FrontendState::new();
5938
5939        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5940        let mock_ctx = ExecutorContext::new_mock(None).await;
5941        let version = Version(0);
5942
5943        frontend.hack_set_program(&ctx, program).await.unwrap();
5944        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5945        let sketch_id = sketch_object.id;
5946        let sketch = expect_sketch(sketch_object);
5947        let line_id = *sketch.segments.get(5).unwrap();
5948
5949        let (src_delta, scene_delta) = frontend
5950            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
5951            .await
5952            .unwrap();
5953        assert_eq!(
5954            src_delta.text.as_str(),
5955            "\
5956@settings(experimentalFeatures = allow)
5957
5958sketch(on = XY) {
5959  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5960}
5961"
5962        );
5963        assert_eq!(
5964            scene_delta.new_graph.objects.len(),
5965            5,
5966            "{:#?}",
5967            scene_delta.new_graph.objects
5968        );
5969
5970        ctx.close().await;
5971        mock_ctx.close().await;
5972    }
5973
5974    #[tokio::test(flavor = "multi_thread")]
5975    async fn test_delete_line_cascades_to_distance_constraint() {
5976        let initial_source = "\
5977@settings(experimentalFeatures = allow)
5978
5979sketch(on = XY) {
5980  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5981  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5982  distance([line1.end, line2.start]) == 10mm
5983}
5984";
5985
5986        let program = Program::parse(initial_source).unwrap().0.unwrap();
5987
5988        let mut frontend = FrontendState::new();
5989
5990        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5991        let mock_ctx = ExecutorContext::new_mock(None).await;
5992        let version = Version(0);
5993
5994        frontend.hack_set_program(&ctx, program).await.unwrap();
5995        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5996        let sketch_id = sketch_object.id;
5997        let sketch = expect_sketch(sketch_object);
5998        let line_id = *sketch.segments.get(5).unwrap();
5999
6000        let (src_delta, scene_delta) = frontend
6001            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
6002            .await
6003            .unwrap();
6004        assert_eq!(
6005            src_delta.text.as_str(),
6006            "\
6007@settings(experimentalFeatures = allow)
6008
6009sketch(on = XY) {
6010  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6011}
6012"
6013        );
6014        assert_eq!(
6015            scene_delta.new_graph.objects.len(),
6016            5,
6017            "{:#?}",
6018            scene_delta.new_graph.objects
6019        );
6020
6021        ctx.close().await;
6022        mock_ctx.close().await;
6023    }
6024
6025    #[tokio::test(flavor = "multi_thread")]
6026    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
6027        let initial_source = "\
6028@settings(experimentalFeatures = allow)
6029
6030sketch(on = XY) {
6031  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6032  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6033  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
6034  equalLength([line1, line2, line3])
6035}
6036";
6037
6038        let program = Program::parse(initial_source).unwrap().0.unwrap();
6039
6040        let mut frontend = FrontendState::new();
6041
6042        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6043        let mock_ctx = ExecutorContext::new_mock(None).await;
6044        let version = Version(0);
6045
6046        frontend.hack_set_program(&ctx, program).await.unwrap();
6047        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6048        let sketch_id = sketch_object.id;
6049        let sketch = expect_sketch(sketch_object);
6050        let line3_id = *sketch.segments.get(8).unwrap();
6051
6052        let (src_delta, scene_delta) = frontend
6053            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
6054            .await
6055            .unwrap();
6056        assert_eq!(
6057            src_delta.text.as_str(),
6058            "\
6059@settings(experimentalFeatures = allow)
6060
6061sketch(on = XY) {
6062  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6063  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
6064  equalLength([line1, line2])
6065}
6066"
6067        );
6068
6069        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6070        let sketch = expect_sketch(sketch_object);
6071        assert_eq!(sketch.constraints.len(), 1);
6072
6073        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
6074        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
6075            panic!("Expected constraint object");
6076        };
6077        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
6078            panic!("Expected lines equal length constraint");
6079        };
6080        assert_eq!(lines_equal_length.lines.len(), 2);
6081
6082        ctx.close().await;
6083        mock_ctx.close().await;
6084    }
6085
6086    #[tokio::test(flavor = "multi_thread")]
6087    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
6088        let initial_source = "\
6089@settings(experimentalFeatures = allow)
6090
6091sketch(on = XY) {
6092  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6093  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6094  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
6095  equalLength([line1, line2, line3])
6096}
6097";
6098
6099        let program = Program::parse(initial_source).unwrap().0.unwrap();
6100
6101        let mut frontend = FrontendState::new();
6102
6103        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6104        let mock_ctx = ExecutorContext::new_mock(None).await;
6105        let version = Version(0);
6106
6107        frontend.hack_set_program(&ctx, program).await.unwrap();
6108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6109        let sketch_id = sketch_object.id;
6110        let sketch = expect_sketch(sketch_object);
6111        let line2_id = *sketch.segments.get(5).unwrap();
6112        let line3_id = *sketch.segments.get(8).unwrap();
6113
6114        let (src_delta, scene_delta) = frontend
6115            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
6116            .await
6117            .unwrap();
6118        assert_eq!(
6119            src_delta.text.as_str(),
6120            "\
6121@settings(experimentalFeatures = allow)
6122
6123sketch(on = XY) {
6124  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6125}
6126"
6127        );
6128
6129        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6130        let sketch = expect_sketch(sketch_object);
6131        assert!(sketch.constraints.is_empty());
6132
6133        ctx.close().await;
6134        mock_ctx.close().await;
6135    }
6136
6137    #[tokio::test(flavor = "multi_thread")]
6138    async fn test_delete_line_line_coincident_constraint() {
6139        let initial_source = "\
6140@settings(experimentalFeatures = allow)
6141
6142sketch(on = XY) {
6143  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6144  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6145  coincident([line1, line2])
6146}
6147";
6148
6149        let program = Program::parse(initial_source).unwrap().0.unwrap();
6150
6151        let mut frontend = FrontendState::new();
6152
6153        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6154        let mock_ctx = ExecutorContext::new_mock(None).await;
6155        let version = Version(0);
6156
6157        frontend.hack_set_program(&ctx, program).await.unwrap();
6158        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6159        let sketch_id = sketch_object.id;
6160        let sketch = expect_sketch(sketch_object);
6161
6162        let coincident_id = *sketch.constraints.first().unwrap();
6163
6164        let (src_delta, scene_delta) = frontend
6165            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
6166            .await
6167            .unwrap();
6168        assert_eq!(
6169            src_delta.text.as_str(),
6170            "\
6171@settings(experimentalFeatures = allow)
6172
6173sketch(on = XY) {
6174  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6175  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
6176}
6177"
6178        );
6179        assert_eq!(scene_delta.new_objects, vec![]);
6180        assert_eq!(scene_delta.new_graph.objects.len(), 8);
6181
6182        ctx.close().await;
6183        mock_ctx.close().await;
6184    }
6185
6186    #[tokio::test(flavor = "multi_thread")]
6187    async fn test_two_points_coincident() {
6188        let initial_source = "\
6189@settings(experimentalFeatures = allow)
6190
6191sketch(on = XY) {
6192  point1 = point(at = [var 1, var 2])
6193  point(at = [3, 4])
6194}
6195";
6196
6197        let program = Program::parse(initial_source).unwrap().0.unwrap();
6198
6199        let mut frontend = FrontendState::new();
6200
6201        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6202        let mock_ctx = ExecutorContext::new_mock(None).await;
6203        let version = Version(0);
6204
6205        frontend.hack_set_program(&ctx, program).await.unwrap();
6206        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6207        let sketch_id = sketch_object.id;
6208        let sketch = expect_sketch(sketch_object);
6209        let point0_id = *sketch.segments.first().unwrap();
6210        let point1_id = *sketch.segments.get(1).unwrap();
6211
6212        let constraint = Constraint::Coincident(Coincident {
6213            segments: vec![point0_id, point1_id],
6214        });
6215        let (src_delta, scene_delta) = frontend
6216            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6217            .await
6218            .unwrap();
6219        assert_eq!(
6220            src_delta.text.as_str(),
6221            "\
6222@settings(experimentalFeatures = allow)
6223
6224sketch(on = XY) {
6225  point1 = point(at = [var 1, var 2])
6226  point2 = point(at = [3, 4])
6227  coincident([point1, point2])
6228}
6229"
6230        );
6231        assert_eq!(
6232            scene_delta.new_graph.objects.len(),
6233            5,
6234            "{:#?}",
6235            scene_delta.new_graph.objects
6236        );
6237
6238        ctx.close().await;
6239        mock_ctx.close().await;
6240    }
6241
6242    #[tokio::test(flavor = "multi_thread")]
6243    async fn test_coincident_of_line_end_points() {
6244        let initial_source = "\
6245@settings(experimentalFeatures = allow)
6246
6247sketch(on = XY) {
6248  line(start = [var 1, var 2], end = [var 3, var 4])
6249  line(start = [var 5, var 6], end = [var 7, var 8])
6250}
6251";
6252
6253        let program = Program::parse(initial_source).unwrap().0.unwrap();
6254
6255        let mut frontend = FrontendState::new();
6256
6257        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6258        let mock_ctx = ExecutorContext::new_mock(None).await;
6259        let version = Version(0);
6260
6261        frontend.hack_set_program(&ctx, program).await.unwrap();
6262        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6263        let sketch_id = sketch_object.id;
6264        let sketch = expect_sketch(sketch_object);
6265        let point0_id = *sketch.segments.get(1).unwrap();
6266        let point1_id = *sketch.segments.get(3).unwrap();
6267
6268        let constraint = Constraint::Coincident(Coincident {
6269            segments: vec![point0_id, point1_id],
6270        });
6271        let (src_delta, scene_delta) = frontend
6272            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6273            .await
6274            .unwrap();
6275        assert_eq!(
6276            src_delta.text.as_str(),
6277            "\
6278@settings(experimentalFeatures = allow)
6279
6280sketch(on = XY) {
6281  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6282  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6283  coincident([line1.end, line2.start])
6284}
6285"
6286        );
6287        assert_eq!(
6288            scene_delta.new_graph.objects.len(),
6289            9,
6290            "{:#?}",
6291            scene_delta.new_graph.objects
6292        );
6293
6294        ctx.close().await;
6295        mock_ctx.close().await;
6296    }
6297
6298    #[tokio::test(flavor = "multi_thread")]
6299    async fn test_invalid_coincident_arc_and_line_preserves_state() {
6300        // Test that attempting an invalid coincident constraint (arc and line)
6301        // doesn't corrupt the state, allowing subsequent operations to work.
6302        // This test verifies the transactional fix in add_constraint that prevents
6303        // state corruption when invalid constraints are attempted.
6304        // Example: coincident constraint between an arc segment and a straight line segment
6305        // is geometrically invalid and should fail, but state should remain intact.
6306        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
6307        let program = Program::empty();
6308
6309        let mut frontend = FrontendState::new();
6310        frontend.program = program;
6311
6312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6313        let mock_ctx = ExecutorContext::new_mock(None).await;
6314        let version = Version(0);
6315
6316        let sketch_args = SketchCtor {
6317            on: Plane::Default(PlaneName::Xy),
6318        };
6319        let (_src_delta, _scene_delta, sketch_id) = frontend
6320            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6321            .await
6322            .unwrap();
6323
6324        // Add an arc segment
6325        let arc_ctor = ArcCtor {
6326            start: Point2d {
6327                x: Expr::Var(Number {
6328                    value: 0.0,
6329                    units: NumericSuffix::Mm,
6330                }),
6331                y: Expr::Var(Number {
6332                    value: 0.0,
6333                    units: NumericSuffix::Mm,
6334                }),
6335            },
6336            end: Point2d {
6337                x: Expr::Var(Number {
6338                    value: 10.0,
6339                    units: NumericSuffix::Mm,
6340                }),
6341                y: Expr::Var(Number {
6342                    value: 10.0,
6343                    units: NumericSuffix::Mm,
6344                }),
6345            },
6346            center: Point2d {
6347                x: Expr::Var(Number {
6348                    value: 10.0,
6349                    units: NumericSuffix::Mm,
6350                }),
6351                y: Expr::Var(Number {
6352                    value: 0.0,
6353                    units: NumericSuffix::Mm,
6354                }),
6355            },
6356            construction: None,
6357        };
6358        let (_src_delta, scene_delta) = frontend
6359            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
6360            .await
6361            .unwrap();
6362        // The arc is the last object in new_objects (after the 3 points: start, end, center)
6363        let arc_id = *scene_delta.new_objects.last().unwrap();
6364
6365        // Add a line segment
6366        let line_ctor = LineCtor {
6367            start: Point2d {
6368                x: Expr::Var(Number {
6369                    value: 20.0,
6370                    units: NumericSuffix::Mm,
6371                }),
6372                y: Expr::Var(Number {
6373                    value: 0.0,
6374                    units: NumericSuffix::Mm,
6375                }),
6376            },
6377            end: Point2d {
6378                x: Expr::Var(Number {
6379                    value: 30.0,
6380                    units: NumericSuffix::Mm,
6381                }),
6382                y: Expr::Var(Number {
6383                    value: 10.0,
6384                    units: NumericSuffix::Mm,
6385                }),
6386            },
6387            construction: None,
6388        };
6389        let (_src_delta, scene_delta) = frontend
6390            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
6391            .await
6392            .unwrap();
6393        // The line is the last object in new_objects (after the 2 points: start, end)
6394        let line_id = *scene_delta.new_objects.last().unwrap();
6395
6396        // Attempt to add an invalid coincident constraint between arc and line
6397        // This should fail during execution, but state should remain intact
6398        let constraint = Constraint::Coincident(Coincident {
6399            segments: vec![arc_id, line_id],
6400        });
6401        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
6402
6403        // The constraint addition should fail (invalid constraint)
6404        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
6405
6406        // Verify state is not corrupted by checking that we can still access the scene graph
6407        // and that the original segments are still present with their source ranges
6408        let sketch_object_after =
6409            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
6410        let sketch_after = expect_sketch(sketch_object_after);
6411
6412        // Verify both segments are still in the sketch
6413        assert!(
6414            sketch_after.segments.contains(&arc_id),
6415            "Arc segment should still exist after failed constraint"
6416        );
6417        assert!(
6418            sketch_after.segments.contains(&line_id),
6419            "Line segment should still exist after failed constraint"
6420        );
6421
6422        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
6423        let arc_obj = frontend
6424            .scene_graph
6425            .objects
6426            .get(arc_id.0)
6427            .expect("Arc object should still be accessible");
6428        let line_obj = frontend
6429            .scene_graph
6430            .objects
6431            .get(line_id.0)
6432            .expect("Line object should still be accessible");
6433
6434        // Verify source ranges are still valid (not corrupted)
6435        // Just verify that the objects are still accessible and have the expected types
6436        match &arc_obj.kind {
6437            ObjectKind::Segment {
6438                segment: Segment::Arc(_),
6439            } => {}
6440            _ => panic!("Arc object should still be an arc segment"),
6441        }
6442        match &line_obj.kind {
6443            ObjectKind::Segment {
6444                segment: Segment::Line(_),
6445            } => {}
6446            _ => panic!("Line object should still be a line segment"),
6447        }
6448
6449        ctx.close().await;
6450        mock_ctx.close().await;
6451    }
6452
6453    #[tokio::test(flavor = "multi_thread")]
6454    async fn test_distance_two_points() {
6455        let initial_source = "\
6456@settings(experimentalFeatures = allow)
6457
6458sketch(on = XY) {
6459  point(at = [var 1, var 2])
6460  point(at = [var 3, var 4])
6461}
6462";
6463
6464        let program = Program::parse(initial_source).unwrap().0.unwrap();
6465
6466        let mut frontend = FrontendState::new();
6467
6468        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6469        let mock_ctx = ExecutorContext::new_mock(None).await;
6470        let version = Version(0);
6471
6472        frontend.hack_set_program(&ctx, program).await.unwrap();
6473        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6474        let sketch_id = sketch_object.id;
6475        let sketch = expect_sketch(sketch_object);
6476        let point0_id = *sketch.segments.first().unwrap();
6477        let point1_id = *sketch.segments.get(1).unwrap();
6478
6479        let constraint = Constraint::Distance(Distance {
6480            points: vec![point0_id, point1_id],
6481            distance: Number {
6482                value: 2.0,
6483                units: NumericSuffix::Mm,
6484            },
6485            source: Default::default(),
6486        });
6487        let (src_delta, scene_delta) = frontend
6488            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6489            .await
6490            .unwrap();
6491        assert_eq!(
6492            src_delta.text.as_str(),
6493            // The lack indentation is a formatter bug.
6494            "\
6495@settings(experimentalFeatures = allow)
6496
6497sketch(on = XY) {
6498  point1 = point(at = [var 1, var 2])
6499  point2 = point(at = [var 3, var 4])
6500  distance([point1, point2]) == 2mm
6501}
6502"
6503        );
6504        assert_eq!(
6505            scene_delta.new_graph.objects.len(),
6506            5,
6507            "{:#?}",
6508            scene_delta.new_graph.objects
6509        );
6510
6511        ctx.close().await;
6512        mock_ctx.close().await;
6513    }
6514
6515    #[tokio::test(flavor = "multi_thread")]
6516    async fn test_horizontal_distance_two_points() {
6517        let initial_source = "\
6518@settings(experimentalFeatures = allow)
6519
6520sketch(on = XY) {
6521  point(at = [var 1, var 2])
6522  point(at = [var 3, var 4])
6523}
6524";
6525
6526        let program = Program::parse(initial_source).unwrap().0.unwrap();
6527
6528        let mut frontend = FrontendState::new();
6529
6530        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6531        let mock_ctx = ExecutorContext::new_mock(None).await;
6532        let version = Version(0);
6533
6534        frontend.hack_set_program(&ctx, program).await.unwrap();
6535        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6536        let sketch_id = sketch_object.id;
6537        let sketch = expect_sketch(sketch_object);
6538        let point0_id = *sketch.segments.first().unwrap();
6539        let point1_id = *sketch.segments.get(1).unwrap();
6540
6541        let constraint = Constraint::HorizontalDistance(Distance {
6542            points: vec![point0_id, point1_id],
6543            distance: Number {
6544                value: 2.0,
6545                units: NumericSuffix::Mm,
6546            },
6547            source: Default::default(),
6548        });
6549        let (src_delta, scene_delta) = frontend
6550            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6551            .await
6552            .unwrap();
6553        assert_eq!(
6554            src_delta.text.as_str(),
6555            // The lack indentation is a formatter bug.
6556            "\
6557@settings(experimentalFeatures = allow)
6558
6559sketch(on = XY) {
6560  point1 = point(at = [var 1, var 2])
6561  point2 = point(at = [var 3, var 4])
6562  horizontalDistance([point1, point2]) == 2mm
6563}
6564"
6565        );
6566        assert_eq!(
6567            scene_delta.new_graph.objects.len(),
6568            5,
6569            "{:#?}",
6570            scene_delta.new_graph.objects
6571        );
6572
6573        ctx.close().await;
6574        mock_ctx.close().await;
6575    }
6576
6577    #[tokio::test(flavor = "multi_thread")]
6578    async fn test_radius_single_arc_segment() {
6579        let initial_source = "\
6580@settings(experimentalFeatures = allow)
6581
6582sketch(on = XY) {
6583  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6584}
6585";
6586
6587        let program = Program::parse(initial_source).unwrap().0.unwrap();
6588
6589        let mut frontend = FrontendState::new();
6590
6591        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6592        let mock_ctx = ExecutorContext::new_mock(None).await;
6593        let version = Version(0);
6594
6595        frontend.hack_set_program(&ctx, program).await.unwrap();
6596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6597        let sketch_id = sketch_object.id;
6598        let sketch = expect_sketch(sketch_object);
6599        // Find the arc segment (not the points)
6600        let arc_id = sketch
6601            .segments
6602            .iter()
6603            .find(|&seg_id| {
6604                let obj = frontend.scene_graph.objects.get(seg_id.0);
6605                matches!(
6606                    obj.map(|o| &o.kind),
6607                    Some(ObjectKind::Segment {
6608                        segment: Segment::Arc(_)
6609                    })
6610                )
6611            })
6612            .unwrap();
6613
6614        let constraint = Constraint::Radius(Radius {
6615            arc: *arc_id,
6616            radius: Number {
6617                value: 5.0,
6618                units: NumericSuffix::Mm,
6619            },
6620            source: Default::default(),
6621        });
6622        let (src_delta, scene_delta) = frontend
6623            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6624            .await
6625            .unwrap();
6626        assert_eq!(
6627            src_delta.text.as_str(),
6628            // The lack indentation is a formatter bug.
6629            "\
6630@settings(experimentalFeatures = allow)
6631
6632sketch(on = XY) {
6633  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6634  radius(arc1) == 5mm
6635}
6636"
6637        );
6638        assert_eq!(
6639            scene_delta.new_graph.objects.len(),
6640            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
6641            "{:#?}",
6642            scene_delta.new_graph.objects
6643        );
6644
6645        ctx.close().await;
6646        mock_ctx.close().await;
6647    }
6648
6649    #[tokio::test(flavor = "multi_thread")]
6650    async fn test_vertical_distance_two_points() {
6651        let initial_source = "\
6652@settings(experimentalFeatures = allow)
6653
6654sketch(on = XY) {
6655  point(at = [var 1, var 2])
6656  point(at = [var 3, var 4])
6657}
6658";
6659
6660        let program = Program::parse(initial_source).unwrap().0.unwrap();
6661
6662        let mut frontend = FrontendState::new();
6663
6664        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6665        let mock_ctx = ExecutorContext::new_mock(None).await;
6666        let version = Version(0);
6667
6668        frontend.hack_set_program(&ctx, program).await.unwrap();
6669        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6670        let sketch_id = sketch_object.id;
6671        let sketch = expect_sketch(sketch_object);
6672        let point0_id = *sketch.segments.first().unwrap();
6673        let point1_id = *sketch.segments.get(1).unwrap();
6674
6675        let constraint = Constraint::VerticalDistance(Distance {
6676            points: vec![point0_id, point1_id],
6677            distance: Number {
6678                value: 2.0,
6679                units: NumericSuffix::Mm,
6680            },
6681            source: Default::default(),
6682        });
6683        let (src_delta, scene_delta) = frontend
6684            .add_constraint(&mock_ctx, version, sketch_id, constraint)
6685            .await
6686            .unwrap();
6687        assert_eq!(
6688            src_delta.text.as_str(),
6689            // The lack indentation is a formatter bug.
6690            "\
6691@settings(experimentalFeatures = allow)
6692
6693sketch(on = XY) {
6694  point1 = point(at = [var 1, var 2])
6695  point2 = point(at = [var 3, var 4])
6696  verticalDistance([point1, point2]) == 2mm
6697}
6698"
6699        );
6700        assert_eq!(
6701            scene_delta.new_graph.objects.len(),
6702            5,
6703            "{:#?}",
6704            scene_delta.new_graph.objects
6705        );
6706
6707        ctx.close().await;
6708        mock_ctx.close().await;
6709    }
6710
6711    #[tokio::test(flavor = "multi_thread")]
6712    async fn test_add_fixed_standalone_point() {
6713        let initial_source = "\
6714@settings(experimentalFeatures = allow)
6715
6716sketch(on = XY) {
6717  point(at = [var 1, var 2])
6718}
6719";
6720
6721        let program = Program::parse(initial_source).unwrap().0.unwrap();
6722
6723        let mut frontend = FrontendState::new();
6724
6725        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6726        let mock_ctx = ExecutorContext::new_mock(None).await;
6727        let version = Version(0);
6728
6729        frontend.hack_set_program(&ctx, program).await.unwrap();
6730        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6731        let sketch_id = sketch_object.id;
6732        let sketch = expect_sketch(sketch_object);
6733        let point_id = *sketch.segments.first().unwrap();
6734
6735        let (src_delta, scene_delta) = frontend
6736            .add_constraint(
6737                &mock_ctx,
6738                version,
6739                sketch_id,
6740                Constraint::Fixed(Fixed {
6741                    points: vec![FixedPoint {
6742                        point: point_id,
6743                        position: Point2d {
6744                            x: Number {
6745                                value: 2.0,
6746                                units: NumericSuffix::Mm,
6747                            },
6748                            y: Number {
6749                                value: 3.0,
6750                                units: NumericSuffix::Mm,
6751                            },
6752                        },
6753                    }],
6754                }),
6755            )
6756            .await
6757            .unwrap();
6758        assert_eq!(
6759            src_delta.text.as_str(),
6760            "\
6761@settings(experimentalFeatures = allow)
6762
6763sketch(on = XY) {
6764  point1 = point(at = [var 1, var 2])
6765  fixed([point1, [2mm, 3mm]])
6766}
6767"
6768        );
6769        assert_eq!(
6770            scene_delta.new_graph.objects.len(),
6771            4,
6772            "{:#?}",
6773            scene_delta.new_graph.objects
6774        );
6775
6776        ctx.close().await;
6777        mock_ctx.close().await;
6778    }
6779
6780    #[tokio::test(flavor = "multi_thread")]
6781    async fn test_add_fixed_multiple_points() {
6782        let initial_source = "\
6783@settings(experimentalFeatures = allow)
6784
6785sketch(on = XY) {
6786  point(at = [var 1, var 2])
6787  point(at = [var 3, var 4])
6788}
6789";
6790
6791        let program = Program::parse(initial_source).unwrap().0.unwrap();
6792
6793        let mut frontend = FrontendState::new();
6794
6795        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6796        let mock_ctx = ExecutorContext::new_mock(None).await;
6797        let version = Version(0);
6798
6799        frontend.hack_set_program(&ctx, program).await.unwrap();
6800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6801        let sketch_id = sketch_object.id;
6802        let sketch = expect_sketch(sketch_object);
6803        let point0_id = *sketch.segments.first().unwrap();
6804        let point1_id = *sketch.segments.get(1).unwrap();
6805
6806        let (src_delta, scene_delta) = frontend
6807            .add_constraint(
6808                &mock_ctx,
6809                version,
6810                sketch_id,
6811                Constraint::Fixed(Fixed {
6812                    points: vec![
6813                        FixedPoint {
6814                            point: point0_id,
6815                            position: Point2d {
6816                                x: Number {
6817                                    value: 2.0,
6818                                    units: NumericSuffix::Mm,
6819                                },
6820                                y: Number {
6821                                    value: 3.0,
6822                                    units: NumericSuffix::Mm,
6823                                },
6824                            },
6825                        },
6826                        FixedPoint {
6827                            point: point1_id,
6828                            position: Point2d {
6829                                x: Number {
6830                                    value: 4.0,
6831                                    units: NumericSuffix::Mm,
6832                                },
6833                                y: Number {
6834                                    value: 5.0,
6835                                    units: NumericSuffix::Mm,
6836                                },
6837                            },
6838                        },
6839                    ],
6840                }),
6841            )
6842            .await
6843            .unwrap();
6844        assert_eq!(
6845            src_delta.text.as_str(),
6846            "\
6847@settings(experimentalFeatures = allow)
6848
6849sketch(on = XY) {
6850  point1 = point(at = [var 1, var 2])
6851  point2 = point(at = [var 3, var 4])
6852  fixed([point1, [2mm, 3mm]])
6853  fixed([point2, [4mm, 5mm]])
6854}
6855"
6856        );
6857        assert_eq!(
6858            scene_delta.new_graph.objects.len(),
6859            6,
6860            "{:#?}",
6861            scene_delta.new_graph.objects
6862        );
6863
6864        ctx.close().await;
6865        mock_ctx.close().await;
6866    }
6867
6868    #[tokio::test(flavor = "multi_thread")]
6869    async fn test_add_fixed_owned_point() {
6870        let initial_source = "\
6871@settings(experimentalFeatures = allow)
6872
6873sketch(on = XY) {
6874  line(start = [var 1, var 2], end = [var 3, var 4])
6875}
6876";
6877
6878        let program = Program::parse(initial_source).unwrap().0.unwrap();
6879
6880        let mut frontend = FrontendState::new();
6881
6882        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6883        let mock_ctx = ExecutorContext::new_mock(None).await;
6884        let version = Version(0);
6885
6886        frontend.hack_set_program(&ctx, program).await.unwrap();
6887        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6888        let sketch_id = sketch_object.id;
6889        let sketch = expect_sketch(sketch_object);
6890        let line_start_id = *sketch.segments.first().unwrap();
6891
6892        let (src_delta, scene_delta) = frontend
6893            .add_constraint(
6894                &mock_ctx,
6895                version,
6896                sketch_id,
6897                Constraint::Fixed(Fixed {
6898                    points: vec![FixedPoint {
6899                        point: line_start_id,
6900                        position: Point2d {
6901                            x: Number {
6902                                value: 2.0,
6903                                units: NumericSuffix::Mm,
6904                            },
6905                            y: Number {
6906                                value: 3.0,
6907                                units: NumericSuffix::Mm,
6908                            },
6909                        },
6910                    }],
6911                }),
6912            )
6913            .await
6914            .unwrap();
6915        assert_eq!(
6916            src_delta.text.as_str(),
6917            "\
6918@settings(experimentalFeatures = allow)
6919
6920sketch(on = XY) {
6921  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6922  fixed([line1.start, [2mm, 3mm]])
6923}
6924"
6925        );
6926        assert_eq!(
6927            scene_delta.new_graph.objects.len(),
6928            6,
6929            "{:#?}",
6930            scene_delta.new_graph.objects
6931        );
6932
6933        ctx.close().await;
6934        mock_ctx.close().await;
6935    }
6936
6937    #[tokio::test(flavor = "multi_thread")]
6938    async fn test_radius_error_cases() {
6939        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6940        let mock_ctx = ExecutorContext::new_mock(None).await;
6941        let version = Version(0);
6942
6943        // Test: Single point should error
6944        let initial_source_point = "\
6945@settings(experimentalFeatures = allow)
6946
6947sketch(on = XY) {
6948  point(at = [var 1, var 2])
6949}
6950";
6951        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
6952        let mut frontend_point = FrontendState::new();
6953        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
6954        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
6955        let sketch_id_point = sketch_object_point.id;
6956        let sketch_point = expect_sketch(sketch_object_point);
6957        let point_id = *sketch_point.segments.first().unwrap();
6958
6959        let constraint_point = Constraint::Radius(Radius {
6960            arc: point_id,
6961            radius: Number {
6962                value: 5.0,
6963                units: NumericSuffix::Mm,
6964            },
6965            source: Default::default(),
6966        });
6967        let result_point = frontend_point
6968            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
6969            .await;
6970        assert!(result_point.is_err(), "Single point should error for radius");
6971
6972        // Test: Single line segment should error (only arc segments supported)
6973        let initial_source_line = "\
6974@settings(experimentalFeatures = allow)
6975
6976sketch(on = XY) {
6977  line(start = [var 1, var 2], end = [var 3, var 4])
6978}
6979";
6980        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
6981        let mut frontend_line = FrontendState::new();
6982        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
6983        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
6984        let sketch_id_line = sketch_object_line.id;
6985        let sketch_line = expect_sketch(sketch_object_line);
6986        let line_id = *sketch_line.segments.first().unwrap();
6987
6988        let constraint_line = Constraint::Radius(Radius {
6989            arc: line_id,
6990            radius: Number {
6991                value: 5.0,
6992                units: NumericSuffix::Mm,
6993            },
6994            source: Default::default(),
6995        });
6996        let result_line = frontend_line
6997            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
6998            .await;
6999        assert!(result_line.is_err(), "Single line segment should error for radius");
7000
7001        ctx.close().await;
7002        mock_ctx.close().await;
7003    }
7004
7005    #[tokio::test(flavor = "multi_thread")]
7006    async fn test_diameter_single_arc_segment() {
7007        let initial_source = "\
7008@settings(experimentalFeatures = allow)
7009
7010sketch(on = XY) {
7011  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
7012}
7013";
7014
7015        let program = Program::parse(initial_source).unwrap().0.unwrap();
7016
7017        let mut frontend = FrontendState::new();
7018
7019        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7020        let mock_ctx = ExecutorContext::new_mock(None).await;
7021        let version = Version(0);
7022
7023        frontend.hack_set_program(&ctx, program).await.unwrap();
7024        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7025        let sketch_id = sketch_object.id;
7026        let sketch = expect_sketch(sketch_object);
7027        // Find the arc segment (not the points)
7028        let arc_id = sketch
7029            .segments
7030            .iter()
7031            .find(|&seg_id| {
7032                let obj = frontend.scene_graph.objects.get(seg_id.0);
7033                matches!(
7034                    obj.map(|o| &o.kind),
7035                    Some(ObjectKind::Segment {
7036                        segment: Segment::Arc(_)
7037                    })
7038                )
7039            })
7040            .unwrap();
7041
7042        let constraint = Constraint::Diameter(Diameter {
7043            arc: *arc_id,
7044            diameter: Number {
7045                value: 10.0,
7046                units: NumericSuffix::Mm,
7047            },
7048            source: Default::default(),
7049        });
7050        let (src_delta, scene_delta) = frontend
7051            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7052            .await
7053            .unwrap();
7054        assert_eq!(
7055            src_delta.text.as_str(),
7056            // The lack indentation is a formatter bug.
7057            "\
7058@settings(experimentalFeatures = allow)
7059
7060sketch(on = XY) {
7061  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
7062  diameter(arc1) == 10mm
7063}
7064"
7065        );
7066        assert_eq!(
7067            scene_delta.new_graph.objects.len(),
7068            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
7069            "{:#?}",
7070            scene_delta.new_graph.objects
7071        );
7072
7073        ctx.close().await;
7074        mock_ctx.close().await;
7075    }
7076
7077    #[tokio::test(flavor = "multi_thread")]
7078    async fn test_diameter_error_cases() {
7079        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7080        let mock_ctx = ExecutorContext::new_mock(None).await;
7081        let version = Version(0);
7082
7083        // Test: Single point should error
7084        let initial_source_point = "\
7085@settings(experimentalFeatures = allow)
7086
7087sketch(on = XY) {
7088  point(at = [var 1, var 2])
7089}
7090";
7091        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
7092        let mut frontend_point = FrontendState::new();
7093        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
7094        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
7095        let sketch_id_point = sketch_object_point.id;
7096        let sketch_point = expect_sketch(sketch_object_point);
7097        let point_id = *sketch_point.segments.first().unwrap();
7098
7099        let constraint_point = Constraint::Diameter(Diameter {
7100            arc: point_id,
7101            diameter: Number {
7102                value: 10.0,
7103                units: NumericSuffix::Mm,
7104            },
7105            source: Default::default(),
7106        });
7107        let result_point = frontend_point
7108            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
7109            .await;
7110        assert!(result_point.is_err(), "Single point should error for diameter");
7111
7112        // Test: Single line segment should error (only arc segments supported)
7113        let initial_source_line = "\
7114@settings(experimentalFeatures = allow)
7115
7116sketch(on = XY) {
7117  line(start = [var 1, var 2], end = [var 3, var 4])
7118}
7119";
7120        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
7121        let mut frontend_line = FrontendState::new();
7122        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
7123        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
7124        let sketch_id_line = sketch_object_line.id;
7125        let sketch_line = expect_sketch(sketch_object_line);
7126        let line_id = *sketch_line.segments.first().unwrap();
7127
7128        let constraint_line = Constraint::Diameter(Diameter {
7129            arc: line_id,
7130            diameter: Number {
7131                value: 10.0,
7132                units: NumericSuffix::Mm,
7133            },
7134            source: Default::default(),
7135        });
7136        let result_line = frontend_line
7137            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
7138            .await;
7139        assert!(result_line.is_err(), "Single line segment should error for diameter");
7140
7141        ctx.close().await;
7142        mock_ctx.close().await;
7143    }
7144
7145    #[tokio::test(flavor = "multi_thread")]
7146    async fn test_line_horizontal() {
7147        let initial_source = "\
7148@settings(experimentalFeatures = allow)
7149
7150sketch(on = XY) {
7151  line(start = [var 1, var 2], end = [var 3, var 4])
7152}
7153";
7154
7155        let program = Program::parse(initial_source).unwrap().0.unwrap();
7156
7157        let mut frontend = FrontendState::new();
7158
7159        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7160        let mock_ctx = ExecutorContext::new_mock(None).await;
7161        let version = Version(0);
7162
7163        frontend.hack_set_program(&ctx, program).await.unwrap();
7164        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7165        let sketch_id = sketch_object.id;
7166        let sketch = expect_sketch(sketch_object);
7167        let line1_id = *sketch.segments.get(2).unwrap();
7168
7169        let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
7170        let (src_delta, scene_delta) = frontend
7171            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7172            .await
7173            .unwrap();
7174        assert_eq!(
7175            src_delta.text.as_str(),
7176            "\
7177@settings(experimentalFeatures = allow)
7178
7179sketch(on = XY) {
7180  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7181  horizontal(line1)
7182}
7183"
7184        );
7185        assert_eq!(
7186            scene_delta.new_graph.objects.len(),
7187            6,
7188            "{:#?}",
7189            scene_delta.new_graph.objects
7190        );
7191
7192        ctx.close().await;
7193        mock_ctx.close().await;
7194    }
7195
7196    #[tokio::test(flavor = "multi_thread")]
7197    async fn test_line_vertical() {
7198        let initial_source = "\
7199@settings(experimentalFeatures = allow)
7200
7201sketch(on = XY) {
7202  line(start = [var 1, var 2], end = [var 3, var 4])
7203}
7204";
7205
7206        let program = Program::parse(initial_source).unwrap().0.unwrap();
7207
7208        let mut frontend = FrontendState::new();
7209
7210        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7211        let mock_ctx = ExecutorContext::new_mock(None).await;
7212        let version = Version(0);
7213
7214        frontend.hack_set_program(&ctx, program).await.unwrap();
7215        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7216        let sketch_id = sketch_object.id;
7217        let sketch = expect_sketch(sketch_object);
7218        let line1_id = *sketch.segments.get(2).unwrap();
7219
7220        let constraint = Constraint::Vertical(Vertical { line: line1_id });
7221        let (src_delta, scene_delta) = frontend
7222            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7223            .await
7224            .unwrap();
7225        assert_eq!(
7226            src_delta.text.as_str(),
7227            "\
7228@settings(experimentalFeatures = allow)
7229
7230sketch(on = XY) {
7231  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7232  vertical(line1)
7233}
7234"
7235        );
7236        assert_eq!(
7237            scene_delta.new_graph.objects.len(),
7238            6,
7239            "{:#?}",
7240            scene_delta.new_graph.objects
7241        );
7242
7243        ctx.close().await;
7244        mock_ctx.close().await;
7245    }
7246
7247    #[tokio::test(flavor = "multi_thread")]
7248    async fn test_lines_equal_length() {
7249        let initial_source = "\
7250@settings(experimentalFeatures = allow)
7251
7252sketch(on = XY) {
7253  line(start = [var 1, var 2], end = [var 3, var 4])
7254  line(start = [var 5, var 6], end = [var 7, var 8])
7255}
7256";
7257
7258        let program = Program::parse(initial_source).unwrap().0.unwrap();
7259
7260        let mut frontend = FrontendState::new();
7261
7262        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7263        let mock_ctx = ExecutorContext::new_mock(None).await;
7264        let version = Version(0);
7265
7266        frontend.hack_set_program(&ctx, program).await.unwrap();
7267        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7268        let sketch_id = sketch_object.id;
7269        let sketch = expect_sketch(sketch_object);
7270        let line1_id = *sketch.segments.get(2).unwrap();
7271        let line2_id = *sketch.segments.get(5).unwrap();
7272
7273        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
7274            lines: vec![line1_id, line2_id],
7275        });
7276        let (src_delta, scene_delta) = frontend
7277            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7278            .await
7279            .unwrap();
7280        assert_eq!(
7281            src_delta.text.as_str(),
7282            "\
7283@settings(experimentalFeatures = allow)
7284
7285sketch(on = XY) {
7286  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7287  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7288  equalLength([line1, line2])
7289}
7290"
7291        );
7292        assert_eq!(
7293            scene_delta.new_graph.objects.len(),
7294            9,
7295            "{:#?}",
7296            scene_delta.new_graph.objects
7297        );
7298
7299        ctx.close().await;
7300        mock_ctx.close().await;
7301    }
7302
7303    #[tokio::test(flavor = "multi_thread")]
7304    async fn test_add_constraint_multi_line_equal_length() {
7305        let initial_source = "\
7306@settings(experimentalFeatures = allow)
7307
7308sketch(on = XY) {
7309  line(start = [var 1, var 2], end = [var 3, var 4])
7310  line(start = [var 5, var 6], end = [var 7, var 8])
7311  line(start = [var 9, var 10], end = [var 11, var 12])
7312}
7313";
7314
7315        let program = Program::parse(initial_source).unwrap().0.unwrap();
7316
7317        let mut frontend = FrontendState::new();
7318        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7319        let mock_ctx = ExecutorContext::new_mock(None).await;
7320        let version = Version(0);
7321
7322        frontend.hack_set_program(&ctx, program).await.unwrap();
7323        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7324        let sketch_id = sketch_object.id;
7325        let sketch = expect_sketch(sketch_object);
7326        let line1_id = *sketch.segments.get(2).unwrap();
7327        let line2_id = *sketch.segments.get(5).unwrap();
7328        let line3_id = *sketch.segments.get(8).unwrap();
7329
7330        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
7331            lines: vec![line1_id, line2_id, line3_id],
7332        });
7333        let (src_delta, scene_delta) = frontend
7334            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7335            .await
7336            .unwrap();
7337        assert_eq!(
7338            src_delta.text.as_str(),
7339            "\
7340@settings(experimentalFeatures = allow)
7341
7342sketch(on = XY) {
7343  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7344  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7345  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7346  equalLength([line1, line2, line3])
7347}
7348"
7349        );
7350        let constraints = scene_delta
7351            .new_graph
7352            .objects
7353            .iter()
7354            .filter_map(|obj| {
7355                let ObjectKind::Constraint { constraint } = &obj.kind else {
7356                    return None;
7357                };
7358                Some(constraint)
7359            })
7360            .collect::<Vec<_>>();
7361
7362        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
7363        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
7364            panic!("expected equal length constraint, got {:?}", constraints[0]);
7365        };
7366        assert_eq!(lines_equal_length.lines.len(), 3);
7367
7368        ctx.close().await;
7369        mock_ctx.close().await;
7370    }
7371
7372    #[tokio::test(flavor = "multi_thread")]
7373    async fn test_lines_parallel() {
7374        let initial_source = "\
7375@settings(experimentalFeatures = allow)
7376
7377sketch(on = XY) {
7378  line(start = [var 1, var 2], end = [var 3, var 4])
7379  line(start = [var 5, var 6], end = [var 7, var 8])
7380}
7381";
7382
7383        let program = Program::parse(initial_source).unwrap().0.unwrap();
7384
7385        let mut frontend = FrontendState::new();
7386
7387        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7388        let mock_ctx = ExecutorContext::new_mock(None).await;
7389        let version = Version(0);
7390
7391        frontend.hack_set_program(&ctx, program).await.unwrap();
7392        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7393        let sketch_id = sketch_object.id;
7394        let sketch = expect_sketch(sketch_object);
7395        let line1_id = *sketch.segments.get(2).unwrap();
7396        let line2_id = *sketch.segments.get(5).unwrap();
7397
7398        let constraint = Constraint::Parallel(Parallel {
7399            lines: vec![line1_id, line2_id],
7400        });
7401        let (src_delta, scene_delta) = frontend
7402            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7403            .await
7404            .unwrap();
7405        assert_eq!(
7406            src_delta.text.as_str(),
7407            "\
7408@settings(experimentalFeatures = allow)
7409
7410sketch(on = XY) {
7411  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7412  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7413  parallel([line1, line2])
7414}
7415"
7416        );
7417        assert_eq!(
7418            scene_delta.new_graph.objects.len(),
7419            9,
7420            "{:#?}",
7421            scene_delta.new_graph.objects
7422        );
7423
7424        ctx.close().await;
7425        mock_ctx.close().await;
7426    }
7427
7428    #[tokio::test(flavor = "multi_thread")]
7429    async fn test_lines_perpendicular() {
7430        let initial_source = "\
7431@settings(experimentalFeatures = allow)
7432
7433sketch(on = XY) {
7434  line(start = [var 1, var 2], end = [var 3, var 4])
7435  line(start = [var 5, var 6], end = [var 7, var 8])
7436}
7437";
7438
7439        let program = Program::parse(initial_source).unwrap().0.unwrap();
7440
7441        let mut frontend = FrontendState::new();
7442
7443        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7444        let mock_ctx = ExecutorContext::new_mock(None).await;
7445        let version = Version(0);
7446
7447        frontend.hack_set_program(&ctx, program).await.unwrap();
7448        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7449        let sketch_id = sketch_object.id;
7450        let sketch = expect_sketch(sketch_object);
7451        let line1_id = *sketch.segments.get(2).unwrap();
7452        let line2_id = *sketch.segments.get(5).unwrap();
7453
7454        let constraint = Constraint::Perpendicular(Perpendicular {
7455            lines: vec![line1_id, line2_id],
7456        });
7457        let (src_delta, scene_delta) = frontend
7458            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7459            .await
7460            .unwrap();
7461        assert_eq!(
7462            src_delta.text.as_str(),
7463            "\
7464@settings(experimentalFeatures = allow)
7465
7466sketch(on = XY) {
7467  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7468  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7469  perpendicular([line1, line2])
7470}
7471"
7472        );
7473        assert_eq!(
7474            scene_delta.new_graph.objects.len(),
7475            9,
7476            "{:#?}",
7477            scene_delta.new_graph.objects
7478        );
7479
7480        ctx.close().await;
7481        mock_ctx.close().await;
7482    }
7483
7484    #[tokio::test(flavor = "multi_thread")]
7485    async fn test_lines_angle() {
7486        let initial_source = "\
7487@settings(experimentalFeatures = allow)
7488
7489sketch(on = XY) {
7490  line(start = [var 1, var 2], end = [var 3, var 4])
7491  line(start = [var 5, var 6], end = [var 7, var 8])
7492}
7493";
7494
7495        let program = Program::parse(initial_source).unwrap().0.unwrap();
7496
7497        let mut frontend = FrontendState::new();
7498
7499        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7500        let mock_ctx = ExecutorContext::new_mock(None).await;
7501        let version = Version(0);
7502
7503        frontend.hack_set_program(&ctx, program).await.unwrap();
7504        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7505        let sketch_id = sketch_object.id;
7506        let sketch = expect_sketch(sketch_object);
7507        let line1_id = *sketch.segments.get(2).unwrap();
7508        let line2_id = *sketch.segments.get(5).unwrap();
7509
7510        let constraint = Constraint::Angle(Angle {
7511            lines: vec![line1_id, line2_id],
7512            angle: Number {
7513                value: 30.0,
7514                units: NumericSuffix::Deg,
7515            },
7516            source: Default::default(),
7517        });
7518        let (src_delta, scene_delta) = frontend
7519            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7520            .await
7521            .unwrap();
7522        assert_eq!(
7523            src_delta.text.as_str(),
7524            // The lack indentation is a formatter bug.
7525            "\
7526@settings(experimentalFeatures = allow)
7527
7528sketch(on = XY) {
7529  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7530  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7531  angle([line1, line2]) == 30deg
7532}
7533"
7534        );
7535        assert_eq!(
7536            scene_delta.new_graph.objects.len(),
7537            9,
7538            "{:#?}",
7539            scene_delta.new_graph.objects
7540        );
7541
7542        ctx.close().await;
7543        mock_ctx.close().await;
7544    }
7545
7546    #[tokio::test(flavor = "multi_thread")]
7547    async fn test_segments_tangent() {
7548        let initial_source = "\
7549@settings(experimentalFeatures = allow)
7550
7551sketch(on = XY) {
7552  line(start = [var 1, var 2], end = [var 3, var 4])
7553  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
7554}
7555";
7556
7557        let program = Program::parse(initial_source).unwrap().0.unwrap();
7558
7559        let mut frontend = FrontendState::new();
7560
7561        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7562        let mock_ctx = ExecutorContext::new_mock(None).await;
7563        let version = Version(0);
7564
7565        frontend.hack_set_program(&ctx, program).await.unwrap();
7566        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7567        let sketch_id = sketch_object.id;
7568        let sketch = expect_sketch(sketch_object);
7569        let line1_id = *sketch.segments.get(2).unwrap();
7570        let arc1_id = *sketch.segments.get(6).unwrap();
7571
7572        let constraint = Constraint::Tangent(Tangent {
7573            input: vec![line1_id, arc1_id],
7574        });
7575        let (src_delta, scene_delta) = frontend
7576            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7577            .await
7578            .unwrap();
7579        assert_eq!(
7580            src_delta.text.as_str(),
7581            "\
7582@settings(experimentalFeatures = allow)
7583
7584sketch(on = XY) {
7585  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7586  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
7587  tangent([line1, arc1])
7588}
7589"
7590        );
7591        assert_eq!(
7592            scene_delta.new_graph.objects.len(),
7593            10,
7594            "{:#?}",
7595            scene_delta.new_graph.objects
7596        );
7597
7598        ctx.close().await;
7599        mock_ctx.close().await;
7600    }
7601
7602    #[tokio::test(flavor = "multi_thread")]
7603    async fn test_sketch_on_face_simple() {
7604        let initial_source = "\
7605@settings(experimentalFeatures = allow)
7606
7607len = 2mm
7608cube = startSketchOn(XY)
7609  |> startProfile(at = [0, 0])
7610  |> line(end = [len, 0], tag = $side)
7611  |> line(end = [0, len])
7612  |> line(end = [-len, 0])
7613  |> line(end = [0, -len])
7614  |> close()
7615  |> extrude(length = len)
7616
7617face = faceOf(cube, face = side)
7618";
7619
7620        let program = Program::parse(initial_source).unwrap().0.unwrap();
7621
7622        let mut frontend = FrontendState::new();
7623
7624        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7625        let mock_ctx = ExecutorContext::new_mock(None).await;
7626        let version = Version(0);
7627
7628        frontend.hack_set_program(&ctx, program).await.unwrap();
7629        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
7630        let face_id = face_object.id;
7631
7632        let sketch_args = SketchCtor {
7633            on: Plane::Object(face_id),
7634        };
7635        let (_src_delta, scene_delta, sketch_id) = frontend
7636            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7637            .await
7638            .unwrap();
7639        assert_eq!(sketch_id, ObjectId(2));
7640        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7641        let sketch_object = &scene_delta.new_graph.objects[2];
7642        assert_eq!(sketch_object.id, ObjectId(2));
7643        assert_eq!(
7644            sketch_object.kind,
7645            ObjectKind::Sketch(Sketch {
7646                args: SketchCtor {
7647                    on: Plane::Object(face_id),
7648                },
7649                plane: face_id,
7650                segments: vec![],
7651                constraints: vec![],
7652            })
7653        );
7654        assert_eq!(scene_delta.new_graph.objects.len(), 8);
7655
7656        ctx.close().await;
7657        mock_ctx.close().await;
7658    }
7659
7660    #[tokio::test(flavor = "multi_thread")]
7661    async fn test_sketch_on_wall_artifact_from_region_extrude() {
7662        let initial_source = "\
7663@settings(experimentalFeatures = allow)
7664
7665s = sketch(on = YZ) {
7666  line1 = line(start = [0, 0], end = [0, 1])
7667  line2 = line(start = [0, 1], end = [1, 1])
7668  line3 = line(start = [1, 1], end = [0, 0])
7669}
7670region001 = region(point = [0.1, 0.1], sketch = s)
7671extrude001 = extrude(region001, length = 5)
7672";
7673
7674        let program = Program::parse(initial_source).unwrap().0.unwrap();
7675
7676        let mut frontend = FrontendState::new();
7677        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7678        let version = Version(0);
7679
7680        frontend.hack_set_program(&ctx, program).await.unwrap();
7681        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
7682
7683        let sketch_args = SketchCtor {
7684            on: Plane::Object(wall_object_id),
7685        };
7686        let (src_delta, _scene_delta, _sketch_id) = frontend
7687            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7688            .await
7689            .unwrap();
7690        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
7691
7692        ctx.close().await;
7693    }
7694
7695    #[tokio::test(flavor = "multi_thread")]
7696    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
7697        let initial_source = "\
7698@settings(experimentalFeatures = allow)
7699
7700sketch001 = sketch(on = YZ) {
7701  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
7702  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
7703  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
7704  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
7705  coincident([line1.end, line2.start])
7706  coincident([line2.end, line3.start])
7707  coincident([line3.end, line4.start])
7708  coincident([line4.end, line1.start])
7709  parallel([line2, line4])
7710  parallel([line3, line1])
7711  perpendicular([line1, line2])
7712  horizontal(line3)
7713  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
7714}
7715region001 = region(point = [3.1, 3.74], sketch = sketch001)
7716extrude001 = extrude(region001, length = 5)
7717";
7718
7719        let program = Program::parse(initial_source).unwrap().0.unwrap();
7720
7721        let mut frontend = FrontendState::new();
7722        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7723        let version = Version(0);
7724
7725        frontend.hack_set_program(&ctx, program).await.unwrap();
7726        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
7727
7728        let sketch_args = SketchCtor {
7729            on: Plane::Object(wall_object_id),
7730        };
7731        let (src_delta, _scene_delta, _sketch_id) = frontend
7732            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7733            .await
7734            .unwrap();
7735        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
7736
7737        ctx.close().await;
7738    }
7739
7740    #[tokio::test(flavor = "multi_thread")]
7741    async fn test_sketch_on_plane_incremental() {
7742        let initial_source = "\
7743@settings(experimentalFeatures = allow)
7744
7745len = 2mm
7746cube = startSketchOn(XY)
7747  |> startProfile(at = [0, 0])
7748  |> line(end = [len, 0], tag = $side)
7749  |> line(end = [0, len])
7750  |> line(end = [-len, 0])
7751  |> line(end = [0, -len])
7752  |> close()
7753  |> extrude(length = len)
7754
7755plane = planeOf(cube, face = side)
7756";
7757
7758        let program = Program::parse(initial_source).unwrap().0.unwrap();
7759
7760        let mut frontend = FrontendState::new();
7761
7762        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7763        let mock_ctx = ExecutorContext::new_mock(None).await;
7764        let version = Version(0);
7765
7766        frontend.hack_set_program(&ctx, program).await.unwrap();
7767        // Find the last plane since the first plane is the XY plane.
7768        let plane_object = frontend
7769            .scene_graph
7770            .objects
7771            .iter()
7772            .rev()
7773            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
7774            .unwrap();
7775        let plane_id = plane_object.id;
7776
7777        let sketch_args = SketchCtor {
7778            on: Plane::Object(plane_id),
7779        };
7780        let (src_delta, scene_delta, sketch_id) = frontend
7781            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7782            .await
7783            .unwrap();
7784        assert_eq!(
7785            src_delta.text.as_str(),
7786            "\
7787@settings(experimentalFeatures = allow)
7788
7789len = 2mm
7790cube = startSketchOn(XY)
7791  |> startProfile(at = [0, 0])
7792  |> line(end = [len, 0], tag = $side)
7793  |> line(end = [0, len])
7794  |> line(end = [-len, 0])
7795  |> line(end = [0, -len])
7796  |> close()
7797  |> extrude(length = len)
7798
7799plane = planeOf(cube, face = side)
7800sketch001 = sketch(on = plane) {
7801}
7802"
7803        );
7804        assert_eq!(sketch_id, ObjectId(2));
7805        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7806        let sketch_object = &scene_delta.new_graph.objects[2];
7807        assert_eq!(sketch_object.id, ObjectId(2));
7808        assert_eq!(
7809            sketch_object.kind,
7810            ObjectKind::Sketch(Sketch {
7811                args: SketchCtor {
7812                    on: Plane::Object(plane_id),
7813                },
7814                plane: plane_id,
7815                segments: vec![],
7816                constraints: vec![],
7817            })
7818        );
7819        assert_eq!(scene_delta.new_graph.objects.len(), 9);
7820
7821        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
7822        assert_eq!(plane_object.id, plane_id);
7823        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
7824
7825        ctx.close().await;
7826        mock_ctx.close().await;
7827    }
7828
7829    #[tokio::test(flavor = "multi_thread")]
7830    async fn test_new_sketch_uses_unique_variable_name() {
7831        let initial_source = "\
7832@settings(experimentalFeatures = allow)
7833
7834sketch1 = sketch(on = XY) {
7835}
7836";
7837
7838        let program = Program::parse(initial_source).unwrap().0.unwrap();
7839
7840        let mut frontend = FrontendState::new();
7841        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7842        let version = Version(0);
7843
7844        frontend.hack_set_program(&ctx, program).await.unwrap();
7845
7846        let sketch_args = SketchCtor {
7847            on: Plane::Default(PlaneName::Yz),
7848        };
7849        let (src_delta, _, _) = frontend
7850            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7851            .await
7852            .unwrap();
7853
7854        assert_eq!(
7855            src_delta.text.as_str(),
7856            "\
7857@settings(experimentalFeatures = allow)
7858
7859sketch1 = sketch(on = XY) {
7860}
7861sketch001 = sketch(on = YZ) {
7862}
7863"
7864        );
7865
7866        ctx.close().await;
7867    }
7868
7869    #[tokio::test(flavor = "multi_thread")]
7870    async fn test_new_sketch_twice_using_same_plane() {
7871        let initial_source = "\
7872@settings(experimentalFeatures = allow)
7873
7874sketch1 = sketch(on = XY) {
7875}
7876";
7877
7878        let program = Program::parse(initial_source).unwrap().0.unwrap();
7879
7880        let mut frontend = FrontendState::new();
7881        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7882        let version = Version(0);
7883
7884        frontend.hack_set_program(&ctx, program).await.unwrap();
7885
7886        let sketch_args = SketchCtor {
7887            on: Plane::Default(PlaneName::Xy),
7888        };
7889        let (src_delta, _, _) = frontend
7890            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7891            .await
7892            .unwrap();
7893
7894        assert_eq!(
7895            src_delta.text.as_str(),
7896            "\
7897@settings(experimentalFeatures = allow)
7898
7899sketch1 = sketch(on = XY) {
7900}
7901sketch001 = sketch(on = XY) {
7902}
7903"
7904        );
7905
7906        ctx.close().await;
7907    }
7908
7909    #[tokio::test(flavor = "multi_thread")]
7910    async fn test_sketch_mode_reuses_cached_on_expression() {
7911        let initial_source = "\
7912@settings(experimentalFeatures = allow)
7913
7914width = 2mm
7915sketch(on = offsetPlane(XY, offset = width)) {
7916  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
7917  distance([line1.start, line1.end]) == width
7918}
7919";
7920        let program = Program::parse(initial_source).unwrap().0.unwrap();
7921
7922        let mut frontend = FrontendState::new();
7923        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7924        let mock_ctx = ExecutorContext::new_mock(None).await;
7925        let version = Version(0);
7926        let project_id = ProjectId(0);
7927        let file_id = FileId(0);
7928
7929        frontend.hack_set_program(&ctx, program).await.unwrap();
7930        let initial_object_count = frontend.scene_graph.objects.len();
7931        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
7932            .expect("Expected sketch object to exist")
7933            .id;
7934
7935        // Entering sketch mode should reuse cached `on` expression state
7936        // (offsetPlane result), not fail or create extra on-surface objects.
7937        let scene_delta = frontend
7938            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7939            .await
7940            .unwrap();
7941        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
7942
7943        // A follow-up sketch-mode execution should keep the same stable object
7944        // graph shape as well.
7945        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
7946        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
7947
7948        ctx.close().await;
7949        mock_ctx.close().await;
7950    }
7951
7952    #[tokio::test(flavor = "multi_thread")]
7953    async fn test_multiple_sketch_blocks() {
7954        let initial_source = "\
7955@settings(experimentalFeatures = allow)
7956
7957// Cube that requires the engine.
7958width = 2
7959sketch001 = startSketchOn(XY)
7960profile001 = startProfile(sketch001, at = [0, 0])
7961  |> yLine(length = width, tag = $seg1)
7962  |> xLine(length = width)
7963  |> yLine(length = -width)
7964  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7965  |> close()
7966extrude001 = extrude(profile001, length = width)
7967
7968// Get a value that requires the engine.
7969x = segLen(seg1)
7970
7971// Triangle with side length 2*x.
7972sketch(on = XY) {
7973  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7974  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
7975  coincident([line1.end, line2.start])
7976  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
7977  coincident([line2.end, line3.start])
7978  coincident([line3.end, line1.start])
7979  equalLength([line3, line1])
7980  equalLength([line1, line2])
7981  distance([line1.start, line1.end]) == 2*x
7982}
7983
7984// Line segment with length x.
7985sketch2 = sketch(on = XY) {
7986  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7987  distance([line1.start, line1.end]) == x
7988}
7989";
7990
7991        let program = Program::parse(initial_source).unwrap().0.unwrap();
7992
7993        let mut frontend = FrontendState::new();
7994
7995        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7996        let mock_ctx = ExecutorContext::new_mock(None).await;
7997        let version = Version(0);
7998        let project_id = ProjectId(0);
7999        let file_id = FileId(0);
8000
8001        frontend.hack_set_program(&ctx, program).await.unwrap();
8002        let sketch_objects = frontend
8003            .scene_graph
8004            .objects
8005            .iter()
8006            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
8007            .collect::<Vec<_>>();
8008        let sketch1_id = sketch_objects.first().unwrap().id;
8009        let sketch2_id = sketch_objects.get(1).unwrap().id;
8010        // First point in sketch1.
8011        let point1_id = ObjectId(sketch1_id.0 + 1);
8012        // First point in sketch2.
8013        let point2_id = ObjectId(sketch2_id.0 + 1);
8014
8015        // Edit the first sketch. Objects before the sketch block should be
8016        // present from execution cache so that we can sketch on prior planes,
8017        // for example. Objects after the first sketch block should not be
8018        // present since those statements are skipped in sketch mode.
8019        //
8020        // - startSketchOn(XY) Plane 1
8021        // - sketch on=XY Plane 1
8022        // - Sketch block 16
8023        let scene_delta = frontend
8024            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
8025            .await
8026            .unwrap();
8027        assert_eq!(
8028            scene_delta.new_graph.objects.len(),
8029            18,
8030            "{:#?}",
8031            scene_delta.new_graph.objects
8032        );
8033
8034        // Edit a point in the first sketch.
8035        let point_ctor = PointCtor {
8036            position: Point2d {
8037                x: Expr::Var(Number {
8038                    value: 1.0,
8039                    units: NumericSuffix::Mm,
8040                }),
8041                y: Expr::Var(Number {
8042                    value: 2.0,
8043                    units: NumericSuffix::Mm,
8044                }),
8045            },
8046        };
8047        let segments = vec![ExistingSegmentCtor {
8048            id: point1_id,
8049            ctor: SegmentCtor::Point(point_ctor),
8050        }];
8051        let (src_delta, _) = frontend
8052            .edit_segments(&mock_ctx, version, sketch1_id, segments)
8053            .await
8054            .unwrap();
8055        // Only the first sketch block changes.
8056        assert_eq!(
8057            src_delta.text.as_str(),
8058            "\
8059@settings(experimentalFeatures = allow)
8060
8061// Cube that requires the engine.
8062width = 2
8063sketch001 = startSketchOn(XY)
8064profile001 = startProfile(sketch001, at = [0, 0])
8065  |> yLine(length = width, tag = $seg1)
8066  |> xLine(length = width)
8067  |> yLine(length = -width)
8068  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
8069  |> close()
8070extrude001 = extrude(profile001, length = width)
8071
8072// Get a value that requires the engine.
8073x = segLen(seg1)
8074
8075// Triangle with side length 2*x.
8076sketch(on = XY) {
8077  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
8078  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
8079  coincident([line1.end, line2.start])
8080  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
8081  coincident([line2.end, line3.start])
8082  coincident([line3.end, line1.start])
8083  equalLength([line3, line1])
8084  equalLength([line1, line2])
8085  distance([line1.start, line1.end]) == 2 * x
8086}
8087
8088// Line segment with length x.
8089sketch2 = sketch(on = XY) {
8090  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
8091  distance([line1.start, line1.end]) == x
8092}
8093"
8094        );
8095
8096        // Execute mock to simulate drag end.
8097        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
8098        // Only the first sketch block changes.
8099        assert_eq!(
8100            src_delta.text.as_str(),
8101            "\
8102@settings(experimentalFeatures = allow)
8103
8104// Cube that requires the engine.
8105width = 2
8106sketch001 = startSketchOn(XY)
8107profile001 = startProfile(sketch001, at = [0, 0])
8108  |> yLine(length = width, tag = $seg1)
8109  |> xLine(length = width)
8110  |> yLine(length = -width)
8111  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
8112  |> close()
8113extrude001 = extrude(profile001, length = width)
8114
8115// Get a value that requires the engine.
8116x = segLen(seg1)
8117
8118// Triangle with side length 2*x.
8119sketch(on = XY) {
8120  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
8121  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
8122  coincident([line1.end, line2.start])
8123  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
8124  coincident([line2.end, line3.start])
8125  coincident([line3.end, line1.start])
8126  equalLength([line3, line1])
8127  equalLength([line1, line2])
8128  distance([line1.start, line1.end]) == 2 * x
8129}
8130
8131// Line segment with length x.
8132sketch2 = sketch(on = XY) {
8133  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
8134  distance([line1.start, line1.end]) == x
8135}
8136"
8137        );
8138        // Exit sketch. Objects from the entire program should be present.
8139        //
8140        // - startSketchOn(XY) Plane 1
8141        // - sketch on=XY Plane 1
8142        // - Sketch block 16
8143        // - sketch on=XY cached
8144        // - Sketch block 5
8145        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
8146        assert_eq!(scene.objects.len(), 29, "{:#?}", scene.objects);
8147
8148        // Edit the second sketch.
8149        //
8150        // - startSketchOn(XY) Plane 1
8151        // - sketch on=XY Plane 1
8152        // - Sketch block 16
8153        // - sketch on=XY cached
8154        // - Sketch block 5
8155        let scene_delta = frontend
8156            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
8157            .await
8158            .unwrap();
8159        assert_eq!(
8160            scene_delta.new_graph.objects.len(),
8161            23,
8162            "{:#?}",
8163            scene_delta.new_graph.objects
8164        );
8165
8166        // Edit a point in the second sketch.
8167        let point_ctor = PointCtor {
8168            position: Point2d {
8169                x: Expr::Var(Number {
8170                    value: 3.0,
8171                    units: NumericSuffix::Mm,
8172                }),
8173                y: Expr::Var(Number {
8174                    value: 4.0,
8175                    units: NumericSuffix::Mm,
8176                }),
8177            },
8178        };
8179        let segments = vec![ExistingSegmentCtor {
8180            id: point2_id,
8181            ctor: SegmentCtor::Point(point_ctor),
8182        }];
8183        let (src_delta, _) = frontend
8184            .edit_segments(&mock_ctx, version, sketch2_id, segments)
8185            .await
8186            .unwrap();
8187        // Only the second sketch block changes.
8188        assert_eq!(
8189            src_delta.text.as_str(),
8190            "\
8191@settings(experimentalFeatures = allow)
8192
8193// Cube that requires the engine.
8194width = 2
8195sketch001 = startSketchOn(XY)
8196profile001 = startProfile(sketch001, at = [0, 0])
8197  |> yLine(length = width, tag = $seg1)
8198  |> xLine(length = width)
8199  |> yLine(length = -width)
8200  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
8201  |> close()
8202extrude001 = extrude(profile001, length = width)
8203
8204// Get a value that requires the engine.
8205x = segLen(seg1)
8206
8207// Triangle with side length 2*x.
8208sketch(on = XY) {
8209  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
8210  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
8211  coincident([line1.end, line2.start])
8212  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
8213  coincident([line2.end, line3.start])
8214  coincident([line3.end, line1.start])
8215  equalLength([line3, line1])
8216  equalLength([line1, line2])
8217  distance([line1.start, line1.end]) == 2 * x
8218}
8219
8220// Line segment with length x.
8221sketch2 = sketch(on = XY) {
8222  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
8223  distance([line1.start, line1.end]) == x
8224}
8225"
8226        );
8227
8228        // Execute mock to simulate drag end.
8229        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
8230        // Only the second sketch block changes.
8231        assert_eq!(
8232            src_delta.text.as_str(),
8233            "\
8234@settings(experimentalFeatures = allow)
8235
8236// Cube that requires the engine.
8237width = 2
8238sketch001 = startSketchOn(XY)
8239profile001 = startProfile(sketch001, at = [0, 0])
8240  |> yLine(length = width, tag = $seg1)
8241  |> xLine(length = width)
8242  |> yLine(length = -width)
8243  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
8244  |> close()
8245extrude001 = extrude(profile001, length = width)
8246
8247// Get a value that requires the engine.
8248x = segLen(seg1)
8249
8250// Triangle with side length 2*x.
8251sketch(on = XY) {
8252  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
8253  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
8254  coincident([line1.end, line2.start])
8255  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
8256  coincident([line2.end, line3.start])
8257  coincident([line3.end, line1.start])
8258  equalLength([line3, line1])
8259  equalLength([line1, line2])
8260  distance([line1.start, line1.end]) == 2 * x
8261}
8262
8263// Line segment with length x.
8264sketch2 = sketch(on = XY) {
8265  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
8266  distance([line1.start, line1.end]) == x
8267}
8268"
8269        );
8270
8271        ctx.close().await;
8272        mock_ctx.close().await;
8273    }
8274}