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