Skip to main content

kcl_lib/
frontend.rs

1use std::{
2    cell::Cell,
3    collections::{HashMap, HashSet},
4    ops::ControlFlow,
5};
6
7use indexmap::IndexMap;
8use kcl_error::SourceRange;
9use kittycad_modeling_cmds::units::UnitLength;
10
11use crate::{
12    ExecOutcome, ExecutorContext, Program,
13    collections::AhashIndexSet,
14    exec::WarningLevel,
15    execution::MockConfig,
16    fmt::format_number_literal,
17    front::{ArcCtor, Distance, Freedom, LinesEqualLength, Parallel, Perpendicular, PointCtor},
18    frontend::{
19        api::{
20            Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
21            SourceRef, Version,
22        },
23        modify::{find_defined_names, next_free_name},
24        sketch::{
25            Coincident, Constraint, Diameter, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Radius, Segment,
26            SegmentCtor, SketchApi, SketchCtor, Vertical,
27        },
28        traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
29    },
30    parsing::ast::types as ast,
31    pretty::NumericSuffix,
32    std::constraints::LinesAtAngleKind,
33    walk::{NodeMut, Visitable},
34};
35
36pub(crate) mod api;
37pub(crate) mod modify;
38pub(crate) mod sketch;
39mod traverse;
40pub(crate) mod trim;
41
42struct ArcSizeConstraintParams {
43    points: Vec<ObjectId>,
44    function_name: &'static str,
45    value: f64,
46    units: NumericSuffix,
47    constraint_type_name: &'static str,
48}
49
50const POINT_FN: &str = "point";
51const POINT_AT_PARAM: &str = "at";
52const LINE_FN: &str = "line";
53const LINE_START_PARAM: &str = "start";
54const LINE_END_PARAM: &str = "end";
55const ARC_FN: &str = "arc";
56const ARC_START_PARAM: &str = "start";
57const ARC_END_PARAM: &str = "end";
58const ARC_CENTER_PARAM: &str = "center";
59
60const COINCIDENT_FN: &str = "coincident";
61const DIAMETER_FN: &str = "diameter";
62const DISTANCE_FN: &str = "distance";
63const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
64const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
65const EQUAL_LENGTH_FN: &str = "equalLength";
66const HORIZONTAL_FN: &str = "horizontal";
67const RADIUS_FN: &str = "radius";
68const VERTICAL_FN: &str = "vertical";
69
70const LINE_PROPERTY_START: &str = "start";
71const LINE_PROPERTY_END: &str = "end";
72
73const ARC_PROPERTY_START: &str = "start";
74const ARC_PROPERTY_END: &str = "end";
75const ARC_PROPERTY_CENTER: &str = "center";
76
77const CONSTRUCTION_PARAM: &str = "construction";
78
79#[derive(Debug, Clone, Copy)]
80enum EditDeleteKind {
81    Edit,
82    DeleteNonSketch,
83}
84
85impl EditDeleteKind {
86    /// Returns true if this edit is any type of deletion.
87    fn is_delete(&self) -> bool {
88        match self {
89            EditDeleteKind::Edit => false,
90            EditDeleteKind::DeleteNonSketch => true,
91        }
92    }
93
94    fn to_change_kind(self) -> ChangeKind {
95        match self {
96            EditDeleteKind::Edit => ChangeKind::Edit,
97            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
98        }
99    }
100}
101
102#[derive(Debug, Clone, Copy)]
103enum ChangeKind {
104    Add,
105    Edit,
106    Delete,
107    None,
108}
109
110#[derive(Debug, Clone)]
111pub struct FrontendState {
112    program: Program,
113    scene_graph: SceneGraph,
114    /// Stores the last known freedom value for each point object.
115    /// This allows us to preserve freedom values when freedom analysis isn't run.
116    point_freedom_cache: HashMap<ObjectId, Freedom>,
117}
118
119impl Default for FrontendState {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl FrontendState {
126    pub fn new() -> Self {
127        Self {
128            program: Program::empty(),
129            scene_graph: SceneGraph {
130                project: ProjectId(0),
131                file: FileId(0),
132                version: Version(0),
133                objects: Default::default(),
134                settings: Default::default(),
135                sketch_mode: Default::default(),
136            },
137            point_freedom_cache: HashMap::new(),
138        }
139    }
140
141    /// Get a reference to the scene graph
142    pub fn scene_graph(&self) -> &SceneGraph {
143        &self.scene_graph
144    }
145
146    pub fn default_length_unit(&self) -> UnitLength {
147        self.program
148            .meta_settings()
149            .ok()
150            .flatten()
151            .map(|settings| settings.default_length_units)
152            .unwrap_or(UnitLength::Millimeters)
153    }
154}
155
156impl SketchApi for FrontendState {
157    async fn execute_mock(
158        &mut self,
159        ctx: &ExecutorContext,
160        _version: Version,
161        sketch: ObjectId,
162    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
163        let mut truncated_program = self.program.clone();
164        self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
165
166        // Execute.
167        let outcome = ctx
168            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
169            .await
170            .map_err(|err| Error {
171                msg: err.error.message().to_owned(),
172            })?;
173        let new_source = source_from_ast(&self.program.ast);
174        let src_delta = SourceDelta { text: new_source };
175        // MockConfig::default() has freedom_analysis: true
176        let outcome = self.update_state_after_exec(outcome, true);
177        let scene_graph_delta = SceneGraphDelta {
178            new_graph: self.scene_graph.clone(),
179            new_objects: Default::default(),
180            invalidates_ids: false,
181            exec_outcome: outcome,
182        };
183        Ok((src_delta, scene_graph_delta))
184    }
185
186    async fn new_sketch(
187        &mut self,
188        ctx: &ExecutorContext,
189        _project: ProjectId,
190        _file: FileId,
191        _version: Version,
192        args: SketchCtor,
193    ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
194        // TODO: Check version.
195
196        // Create updated KCL source from args.
197        let plane_ast = ast_name_expr(args.on);
198        let sketch_ast = ast::SketchBlock {
199            arguments: vec![ast::LabeledArg {
200                label: Some(ast::Identifier::new("on")),
201                arg: plane_ast,
202            }],
203            body: Default::default(),
204            is_being_edited: false,
205            non_code_meta: Default::default(),
206            digest: None,
207        };
208        let mut new_ast = self.program.ast.clone();
209        // Ensure that we allow experimental features since the sketch block
210        // won't work without it.
211        new_ast.set_experimental_features(Some(WarningLevel::Allow));
212        // Add a sketch block.
213        new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
214            inner: ast::ExpressionStatement {
215                expression: ast::Expr::SketchBlock(Box::new(ast::Node {
216                    inner: sketch_ast,
217                    start: Default::default(),
218                    end: Default::default(),
219                    module_id: Default::default(),
220                    outer_attrs: Default::default(),
221                    pre_comments: Default::default(),
222                    comment_start: Default::default(),
223                })),
224                digest: None,
225            },
226            start: Default::default(),
227            end: Default::default(),
228            module_id: Default::default(),
229            outer_attrs: Default::default(),
230            pre_comments: Default::default(),
231            comment_start: Default::default(),
232        }));
233        // Convert to string source to create real source ranges.
234        let new_source = source_from_ast(&new_ast);
235        // Parse the new source.
236        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
237        if !errors.is_empty() {
238            return Err(Error {
239                msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
240            });
241        }
242        let Some(new_program) = new_program else {
243            return Err(Error {
244                msg: "No AST produced after adding sketch".to_owned(),
245            });
246        };
247
248        // Make sure to only set this if there are no errors.
249        self.program = new_program.clone();
250
251        // We need to do an engine execute so that the plane object gets created
252        // and is cached.
253        let outcome = ctx.run_with_caching(new_program.clone()).await.map_err(|err| Error {
254            msg: err.error.message().to_owned(),
255        })?;
256        let freedom_analysis_ran = true;
257
258        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
259
260        let Some(sketch_id) = self.scene_graph.objects.last().map(|object| object.id) else {
261            return Err(Error {
262                msg: "No objects in scene graph after adding sketch".to_owned(),
263            });
264        };
265        // Store the object in the scene.
266        self.scene_graph.sketch_mode = Some(sketch_id);
267
268        let src_delta = SourceDelta { text: new_source };
269        let scene_graph_delta = SceneGraphDelta {
270            new_graph: self.scene_graph.clone(),
271            invalidates_ids: false,
272            new_objects: vec![sketch_id],
273            exec_outcome: outcome,
274        };
275        Ok((src_delta, scene_graph_delta, sketch_id))
276    }
277
278    async fn edit_sketch(
279        &mut self,
280        ctx: &ExecutorContext,
281        _project: ProjectId,
282        _file: FileId,
283        _version: Version,
284        sketch: ObjectId,
285    ) -> api::Result<SceneGraphDelta> {
286        // TODO: Check version.
287
288        // Look up existing sketch.
289        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
290            msg: format!("Sketch not found: {sketch:?}"),
291        })?;
292        let ObjectKind::Sketch(_) = &sketch_object.kind else {
293            return Err(Error {
294                msg: format!("Object is not a sketch: {sketch_object:?}"),
295            });
296        };
297
298        // Enter sketch mode by setting the sketch_mode.
299        self.scene_graph.sketch_mode = Some(sketch);
300
301        // Truncate after the sketch block for mock execution.
302        let mut truncated_program = self.program.clone();
303        self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
304
305        // Execute in mock mode to ensure state is up to date. The caller will
306        // want freedom analysis to display segments correctly.
307        let outcome = ctx
308            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
309            .await
310            .map_err(|err| {
311                // TODO: sketch-api: Yeah, this needs to change. We need to
312                // return the full error.
313                Error {
314                    msg: err.error.message().to_owned(),
315                }
316            })?;
317
318        // MockConfig::default() has freedom_analysis: true
319        let outcome = self.update_state_after_exec(outcome, true);
320        let scene_graph_delta = SceneGraphDelta {
321            new_graph: self.scene_graph.clone(),
322            invalidates_ids: false,
323            new_objects: Vec::new(),
324            exec_outcome: outcome,
325        };
326        Ok(scene_graph_delta)
327    }
328
329    async fn exit_sketch(
330        &mut self,
331        ctx: &ExecutorContext,
332        _version: Version,
333        sketch: ObjectId,
334    ) -> api::Result<SceneGraph> {
335        // TODO: Check version.
336        #[cfg(not(target_arch = "wasm32"))]
337        let _ = sketch;
338        #[cfg(target_arch = "wasm32")]
339        if self.scene_graph.sketch_mode != Some(sketch) {
340            web_sys::console::warn_1(
341                &format!(
342                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
343                    &self.scene_graph.sketch_mode
344                )
345                .into(),
346            );
347        }
348        self.scene_graph.sketch_mode = None;
349
350        // Execute.
351        let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
352            // TODO: sketch-api: Yeah, this needs to change. We need to
353            // return the full error.
354            Error {
355                msg: err.error.message().to_owned(),
356            }
357        })?;
358
359        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
360        self.update_state_after_exec(outcome, false);
361
362        Ok(self.scene_graph.clone())
363    }
364
365    async fn delete_sketch(
366        &mut self,
367        ctx: &ExecutorContext,
368        _version: Version,
369        sketch: ObjectId,
370    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
371        // TODO: Check version.
372
373        let mut new_ast = self.program.ast.clone();
374
375        // Look up existing sketch.
376        let sketch_id = sketch;
377        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
378            msg: format!("Sketch not found: {sketch:?}"),
379        })?;
380        let ObjectKind::Sketch(_) = &sketch_object.kind else {
381            return Err(Error {
382                msg: format!("Object is not a sketch: {sketch_object:?}"),
383            });
384        };
385
386        // Modify the AST to remove the sketch.
387        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
388
389        self.execute_after_delete_sketch(ctx, &mut new_ast).await
390    }
391
392    async fn add_segment(
393        &mut self,
394        ctx: &ExecutorContext,
395        _version: Version,
396        sketch: ObjectId,
397        segment: SegmentCtor,
398        _label: Option<String>,
399    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
400        // TODO: Check version.
401        match segment {
402            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
403            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
404            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
405            _ => Err(Error {
406                msg: format!("segment ctor not implemented yet: {segment:?}"),
407            }),
408        }
409    }
410
411    async fn edit_segments(
412        &mut self,
413        ctx: &ExecutorContext,
414        _version: Version,
415        sketch: ObjectId,
416        segments: Vec<ExistingSegmentCtor>,
417    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
418        // TODO: Check version.
419        let mut new_ast = self.program.ast.clone();
420        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
421
422        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
423        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
424        for segment in &segments {
425            segment_ids_edited.insert(segment.id);
426        }
427
428        // Preprocess segments into a final_edits vector to handle if segments contains:
429        // - edit start point of line1 (as SegmentCtor::Point)
430        // - edit end point of line1 (as SegmentCtor::Point)
431        //
432        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
433        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
434        //
435        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
436        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
437        // so the above example would result in a single line1 edit:
438        // - the first start point edit creates a new line edit entry in final_edits
439        // - the second end point edit finds this line edit and mutates the end position only.
440        //
441        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
442        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
443
444        for segment in segments {
445            let segment_id = segment.id;
446            match segment.ctor {
447                SegmentCtor::Point(ctor) => {
448                    // Find the owner, if any (point -> line / arc)
449                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
450                        && let ObjectKind::Segment { segment } = &segment_object.kind
451                        && let Segment::Point(point) = segment
452                        && let Some(owner_id) = point.owner
453                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
454                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
455                    {
456                        match owner_segment {
457                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
458                                if let Some(existing) = final_edits.get_mut(&owner_id) {
459                                    let SegmentCtor::Line(line_ctor) = existing else {
460                                        return Err(Error {
461                                            msg: format!("Internal: Expected line ctor for owner: {owner_object:?}"),
462                                        });
463                                    };
464                                    // Line owner is already in final_edits -> apply this point edit
465                                    if line.start == segment_id {
466                                        line_ctor.start = ctor.position;
467                                    } else {
468                                        line_ctor.end = ctor.position;
469                                    }
470                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
471                                    // Line owner is not in final_edits yet -> create it
472                                    let mut line_ctor = line_ctor.clone();
473                                    if line.start == segment_id {
474                                        line_ctor.start = ctor.position;
475                                    } else {
476                                        line_ctor.end = ctor.position;
477                                    }
478                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
479                                } else {
480                                    // This should never run..
481                                    return Err(Error {
482                                        msg: format!("Internal: Line does not have line ctor: {owner_object:?}"),
483                                    });
484                                }
485                                continue;
486                            }
487                            Segment::Arc(arc)
488                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
489                            {
490                                if let Some(existing) = final_edits.get_mut(&owner_id) {
491                                    let SegmentCtor::Arc(arc_ctor) = existing else {
492                                        return Err(Error {
493                                            msg: format!("Internal: Expected arc ctor for owner: {owner_object:?}"),
494                                        });
495                                    };
496                                    if arc.start == segment_id {
497                                        arc_ctor.start = ctor.position;
498                                    } else if arc.end == segment_id {
499                                        arc_ctor.end = ctor.position;
500                                    } else {
501                                        arc_ctor.center = ctor.position;
502                                    }
503                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
504                                    let mut arc_ctor = arc_ctor.clone();
505                                    if arc.start == segment_id {
506                                        arc_ctor.start = ctor.position;
507                                    } else if arc.end == segment_id {
508                                        arc_ctor.end = ctor.position;
509                                    } else {
510                                        arc_ctor.center = ctor.position;
511                                    }
512                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
513                                } else {
514                                    return Err(Error {
515                                        msg: format!("Internal: Arc does not have arc ctor: {owner_object:?}"),
516                                    });
517                                }
518                                continue;
519                            }
520                            _ => {}
521                        }
522                    }
523
524                    // No owner, it's an individual point
525                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
526                }
527                SegmentCtor::Line(ctor) => {
528                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
529                }
530                SegmentCtor::Arc(ctor) => {
531                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
532                }
533                other_ctor => {
534                    final_edits.insert(segment_id, other_ctor);
535                }
536            }
537        }
538
539        for (segment_id, ctor) in final_edits {
540            match ctor {
541                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment_id, ctor)?,
542                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment_id, ctor)?,
543                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment_id, ctor)?,
544                _ => {
545                    return Err(Error {
546                        msg: format!("segment ctor not implemented yet: {ctor:?}"),
547                    });
548                }
549            }
550        }
551        self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
552            .await
553    }
554
555    async fn delete_objects(
556        &mut self,
557        ctx: &ExecutorContext,
558        _version: Version,
559        sketch: ObjectId,
560        constraint_ids: Vec<ObjectId>,
561        segment_ids: Vec<ObjectId>,
562    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
563        // TODO: Check version.
564
565        // Deduplicate IDs.
566        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
567        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
568
569        // If a point is owned by a Line/Arc, we want to delete the owner, which will
570        // also delete the point, as well as other points that are owned by the owner.
571        let mut delete_ids = AhashIndexSet::default();
572
573        for segment_id in segment_ids_set.iter().copied() {
574            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
575                && let ObjectKind::Segment { segment } = &segment_object.kind
576                && let Segment::Point(point) = segment
577                && let Some(owner_id) = point.owner
578                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
579                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
580                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_))
581            {
582                // segment is owned -> delete the owner
583                delete_ids.insert(owner_id);
584            } else {
585                // segment is not owned by anything -> can be deleted
586                delete_ids.insert(segment_id);
587            }
588        }
589        // Find constraints that reference the segments to be deleted, and add
590        // those to the set to be deleted.
591        self.add_dependent_constraints_to_delete(sketch, &delete_ids, &mut constraint_ids_set)?;
592
593        let mut new_ast = self.program.ast.clone();
594
595        for constraint_id in constraint_ids_set {
596            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
597        }
598        for segment_id in delete_ids {
599            self.delete_segment(&mut new_ast, sketch, segment_id)?;
600        }
601
602        self.execute_after_edit(
603            ctx,
604            sketch,
605            Default::default(),
606            EditDeleteKind::DeleteNonSketch,
607            &mut new_ast,
608        )
609        .await
610    }
611
612    async fn add_constraint(
613        &mut self,
614        ctx: &ExecutorContext,
615        _version: Version,
616        sketch: ObjectId,
617        constraint: Constraint,
618    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
619        // TODO: Check version.
620
621        // Save the original state as a backup - we'll restore it if anything fails
622        let original_program = self.program.clone();
623        let original_scene_graph = self.scene_graph.clone();
624
625        let mut new_ast = self.program.ast.clone();
626        let sketch_block_range = match constraint {
627            Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
628            Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
629            Constraint::HorizontalDistance(distance) => {
630                self.add_horizontal_distance(sketch, distance, &mut new_ast).await?
631            }
632            Constraint::VerticalDistance(distance) => {
633                self.add_vertical_distance(sketch, distance, &mut new_ast).await?
634            }
635            Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
636            Constraint::LinesEqualLength(lines_equal_length) => {
637                self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
638                    .await?
639            }
640            Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
641            Constraint::Perpendicular(perpendicular) => {
642                self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
643            }
644            Constraint::Radius(radius) => self.add_radius(sketch, radius, &mut new_ast).await?,
645            Constraint::Diameter(diameter) => self.add_diameter(sketch, diameter, &mut new_ast).await?,
646            Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
647        };
648
649        let result = self
650            .execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
651            .await;
652
653        // If execution failed, restore the original state to prevent corruption
654        if result.is_err() {
655            self.program = original_program;
656            self.scene_graph = original_scene_graph;
657        }
658
659        result
660    }
661
662    async fn chain_segment(
663        &mut self,
664        ctx: &ExecutorContext,
665        version: Version,
666        sketch: ObjectId,
667        previous_segment_end_point_id: ObjectId,
668        segment: SegmentCtor,
669        _label: Option<String>,
670    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
671        // TODO: Check version.
672
673        // First, add the segment (line) to get its start point ID
674        let SegmentCtor::Line(line_ctor) = segment else {
675            return Err(Error {
676                msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
677            });
678        };
679
680        // Add the line segment first - this updates self.program and self.scene_graph
681        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
682
683        // Find the new line's start point ID from the updated scene graph
684        // add_line updates self.scene_graph, so we can use that
685        let new_line_id = first_scene_delta
686            .new_objects
687            .iter()
688            .find(|&obj_id| {
689                let obj = self.scene_graph.objects.get(obj_id.0);
690                if let Some(obj) = obj {
691                    matches!(
692                        &obj.kind,
693                        ObjectKind::Segment {
694                            segment: Segment::Line(_)
695                        }
696                    )
697                } else {
698                    false
699                }
700            })
701            .ok_or_else(|| Error {
702                msg: "Failed to find new line segment in scene graph".to_string(),
703            })?;
704
705        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
706            msg: format!("New line object not found: {new_line_id:?}"),
707        })?;
708
709        let ObjectKind::Segment {
710            segment: new_line_segment,
711        } = &new_line_obj.kind
712        else {
713            return Err(Error {
714                msg: format!("Object is not a segment: {new_line_obj:?}"),
715            });
716        };
717
718        let Segment::Line(new_line) = new_line_segment else {
719            return Err(Error {
720                msg: format!("Segment is not a line: {new_line_segment:?}"),
721            });
722        };
723
724        let new_line_start_point_id = new_line.start;
725
726        // Now add the coincident constraint between the previous end point and the new line's start point.
727        let coincident = Coincident {
728            segments: vec![previous_segment_end_point_id, new_line_start_point_id],
729        };
730
731        let (final_src_delta, final_scene_delta) = self
732            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
733            .await?;
734
735        // Combine new objects from the line addition and the constraint addition.
736        // Both add_line and add_constraint now populate new_objects correctly.
737        let mut combined_new_objects = first_scene_delta.new_objects.clone();
738        combined_new_objects.extend(final_scene_delta.new_objects);
739
740        let scene_graph_delta = SceneGraphDelta {
741            new_graph: self.scene_graph.clone(),
742            invalidates_ids: false,
743            new_objects: combined_new_objects,
744            exec_outcome: final_scene_delta.exec_outcome,
745        };
746
747        Ok((final_src_delta, scene_graph_delta))
748    }
749
750    async fn edit_constraint(
751        &mut self,
752        _ctx: &ExecutorContext,
753        _version: Version,
754        _sketch: ObjectId,
755        _constraint_id: ObjectId,
756        _constraint: Constraint,
757    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
758        todo!()
759    }
760
761    /// Splitting a segment means creating a new segment, editing the old one, and then
762    /// migrating a bunch of the constraints from the original segment to the new one
763    /// (i.e. deleting them and re-adding them on the other segment).
764    ///
765    /// To keep this efficient we require as few executions as possible: we create the
766    /// new segment first (to get its id), then do all edits and new constraints, and
767    /// do all deletes at the end (since deletes invalidate ids).
768    async fn batch_split_segment_operations(
769        &mut self,
770        ctx: &ExecutorContext,
771        _version: Version,
772        sketch: ObjectId,
773        edit_segments: Vec<ExistingSegmentCtor>,
774        add_constraints: Vec<Constraint>,
775        delete_constraint_ids: Vec<ObjectId>,
776        _new_segment_info: sketch::NewSegmentInfo,
777    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
778        // TODO: Check version.
779        let mut new_ast = self.program.ast.clone();
780        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
781
782        // Step 1: Edit segments
783        for segment in edit_segments {
784            segment_ids_edited.insert(segment.id);
785            match segment.ctor {
786                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
787                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
788                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
789                _ => {
790                    return Err(Error {
791                        msg: format!("segment ctor not implemented yet: {segment:?}"),
792                    });
793                }
794            }
795        }
796
797        // Step 2: Add all constraints
798        for constraint in add_constraints {
799            match constraint {
800                Constraint::Coincident(coincident) => {
801                    self.add_coincident(sketch, coincident, &mut new_ast).await?;
802                }
803                Constraint::Distance(distance) => {
804                    self.add_distance(sketch, distance, &mut new_ast).await?;
805                }
806                Constraint::HorizontalDistance(distance) => {
807                    self.add_horizontal_distance(sketch, distance, &mut new_ast).await?;
808                }
809                Constraint::VerticalDistance(distance) => {
810                    self.add_vertical_distance(sketch, distance, &mut new_ast).await?;
811                }
812                Constraint::Horizontal(horizontal) => {
813                    self.add_horizontal(sketch, horizontal, &mut new_ast).await?;
814                }
815                Constraint::LinesEqualLength(lines_equal_length) => {
816                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
817                        .await?;
818                }
819                Constraint::Parallel(parallel) => {
820                    self.add_parallel(sketch, parallel, &mut new_ast).await?;
821                }
822                Constraint::Perpendicular(perpendicular) => {
823                    self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?;
824                }
825                Constraint::Vertical(vertical) => {
826                    self.add_vertical(sketch, vertical, &mut new_ast).await?;
827                }
828                Constraint::Diameter(diameter) => {
829                    self.add_diameter(sketch, diameter, &mut new_ast).await?;
830                }
831                Constraint::Radius(radius) => {
832                    self.add_radius(sketch, radius, &mut new_ast).await?;
833                }
834            }
835        }
836
837        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
838        let mut constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
839        let segment_ids_set = AhashIndexSet::default();
840        // Find constraints that reference segments to be deleted, and add those to the set to be deleted.
841        self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
842
843        let has_constraint_deletions = !constraint_ids_set.is_empty();
844        for constraint_id in constraint_ids_set {
845            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
846        }
847
848        // Step 4: Execute once at the end
849        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
850        // But we'll manually set invalidates_ids: true if we deleted constraints
851        let (source_delta, mut scene_graph_delta) = self
852            .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
853            .await?;
854
855        // If we deleted constraints, set invalidates_ids: true
856        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
857        if has_constraint_deletions {
858            scene_graph_delta.invalidates_ids = true;
859        }
860
861        Ok((source_delta, scene_graph_delta))
862    }
863
864    async fn batch_tail_cut_operations(
865        &mut self,
866        ctx: &ExecutorContext,
867        _version: Version,
868        sketch: ObjectId,
869        edit_segments: Vec<ExistingSegmentCtor>,
870        add_constraints: Vec<Constraint>,
871        delete_constraint_ids: Vec<ObjectId>,
872    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
873        let mut new_ast = self.program.ast.clone();
874        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
875
876        // Step 1: Edit segments (usually a single segment for tail cut)
877        for segment in edit_segments {
878            segment_ids_edited.insert(segment.id);
879            match segment.ctor {
880                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
881                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
882                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
883                _ => {
884                    return Err(Error {
885                        msg: format!("segment ctor not implemented yet: {segment:?}"),
886                    });
887                }
888            }
889        }
890
891        // Step 2: Add coincident constraints
892        for constraint in add_constraints {
893            match constraint {
894                Constraint::Coincident(coincident) => {
895                    self.add_coincident(sketch, coincident, &mut new_ast).await?;
896                }
897                other => {
898                    return Err(Error {
899                        msg: format!("unsupported constraint in tail cut batch: {other:?}"),
900                    });
901                }
902            }
903        }
904
905        // Step 3: Delete constraints (if any)
906        let mut constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
907        let segment_ids_set = AhashIndexSet::default();
908        self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
909
910        let has_constraint_deletions = !constraint_ids_set.is_empty();
911        for constraint_id in constraint_ids_set {
912            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
913        }
914
915        // Step 4: Single execute_after_edit
916        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
917        // But we'll manually set invalidates_ids: true if we deleted constraints
918        let (source_delta, mut scene_graph_delta) = self
919            .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
920            .await?;
921
922        // If we deleted constraints, set invalidates_ids: true
923        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
924        if has_constraint_deletions {
925            scene_graph_delta.invalidates_ids = true;
926        }
927
928        Ok((source_delta, scene_graph_delta))
929    }
930}
931
932impl FrontendState {
933    pub async fn hack_set_program(
934        &mut self,
935        ctx: &ExecutorContext,
936        program: Program,
937    ) -> api::Result<(SceneGraph, ExecOutcome)> {
938        self.program = program.clone();
939
940        // Execute so that the objects are updated and available for the next
941        // API call.
942        // This always uses engine execution (not mock) so that things are cached.
943        // Engine execution now runs freedom analysis automatically.
944        // Clear the freedom cache since IDs might have changed after direct editing
945        // and we're about to run freedom analysis which will repopulate it.
946        self.point_freedom_cache.clear();
947        let outcome = ctx.run_with_caching(program).await.map_err(|err| Error {
948            msg: err.error.message().to_owned(),
949        })?;
950        let freedom_analysis_ran = true;
951
952        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
953
954        Ok((self.scene_graph.clone(), outcome))
955    }
956
957    async fn add_point(
958        &mut self,
959        ctx: &ExecutorContext,
960        sketch: ObjectId,
961        ctor: PointCtor,
962    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
963        // Create updated KCL source from args.
964        let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
965        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
966            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
967            unlabeled: None,
968            arguments: vec![ast::LabeledArg {
969                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
970                arg: at_ast,
971            }],
972            digest: None,
973            non_code_meta: Default::default(),
974        })));
975
976        // Look up existing sketch.
977        let sketch_id = sketch;
978        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
979            #[cfg(target_arch = "wasm32")]
980            web_sys::console::error_1(
981                &format!(
982                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
983                    &self.scene_graph.objects
984                )
985                .into(),
986            );
987            Error {
988                msg: format!("Sketch not found: {sketch:?}"),
989            }
990        })?;
991        let ObjectKind::Sketch(_) = &sketch_object.kind else {
992            return Err(Error {
993                msg: format!("Object is not a sketch: {sketch_object:?}"),
994            });
995        };
996        // Add the point to the AST of the sketch block.
997        let mut new_ast = self.program.ast.clone();
998        let (sketch_block_range, _) = self.mutate_ast(
999            &mut new_ast,
1000            sketch_id,
1001            AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1002        )?;
1003        // Convert to string source to create real source ranges.
1004        let new_source = source_from_ast(&new_ast);
1005        // Parse the new KCL source.
1006        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1007        if !errors.is_empty() {
1008            return Err(Error {
1009                msg: format!("Error parsing KCL source after adding point: {errors:?}"),
1010            });
1011        }
1012        let Some(new_program) = new_program else {
1013            return Err(Error {
1014                msg: "No AST produced after adding point".to_string(),
1015            });
1016        };
1017
1018        let point_source_range =
1019            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1020                msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
1021            })?;
1022        #[cfg(not(feature = "artifact-graph"))]
1023        let _ = point_source_range;
1024
1025        // Make sure to only set this if there are no errors.
1026        self.program = new_program.clone();
1027
1028        // Truncate after the sketch block for mock execution.
1029        let mut truncated_program = new_program;
1030        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1031
1032        // Execute.
1033        let outcome = ctx
1034            .run_mock(
1035                &truncated_program,
1036                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1037            )
1038            .await
1039            .map_err(|err| {
1040                // TODO: sketch-api: Yeah, this needs to change. We need to
1041                // return the full error.
1042                Error {
1043                    msg: err.error.message().to_owned(),
1044                }
1045            })?;
1046
1047        #[cfg(not(feature = "artifact-graph"))]
1048        let new_object_ids = Vec::new();
1049        #[cfg(feature = "artifact-graph")]
1050        let new_object_ids = {
1051            let segment_id = outcome
1052                .source_range_to_object
1053                .get(&point_source_range)
1054                .copied()
1055                .ok_or_else(|| Error {
1056                    msg: format!("Source range of point not found: {point_source_range:?}"),
1057                })?;
1058            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1059                msg: format!("Segment not found: {segment_id:?}"),
1060            })?;
1061            let ObjectKind::Segment { segment } = &segment_object.kind else {
1062                return Err(Error {
1063                    msg: format!("Object is not a segment: {segment_object:?}"),
1064                });
1065            };
1066            let Segment::Point(_) = segment else {
1067                return Err(Error {
1068                    msg: format!("Segment is not a point: {segment:?}"),
1069                });
1070            };
1071            vec![segment_id]
1072        };
1073        let src_delta = SourceDelta { text: new_source };
1074        // Uses .no_freedom_analysis() so freedom_analysis: false
1075        let outcome = self.update_state_after_exec(outcome, false);
1076        let scene_graph_delta = SceneGraphDelta {
1077            new_graph: self.scene_graph.clone(),
1078            invalidates_ids: false,
1079            new_objects: new_object_ids,
1080            exec_outcome: outcome,
1081        };
1082        Ok((src_delta, scene_graph_delta))
1083    }
1084
1085    async fn add_line(
1086        &mut self,
1087        ctx: &ExecutorContext,
1088        sketch: ObjectId,
1089        ctor: LineCtor,
1090    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1091        // Create updated KCL source from args.
1092        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1093        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1094        let mut arguments = vec![
1095            ast::LabeledArg {
1096                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1097                arg: start_ast,
1098            },
1099            ast::LabeledArg {
1100                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1101                arg: end_ast,
1102            },
1103        ];
1104        // Add construction kwarg if construction is Some(true)
1105        if ctor.construction == Some(true) {
1106            arguments.push(ast::LabeledArg {
1107                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1108                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1109                    value: ast::LiteralValue::Bool(true),
1110                    raw: "true".to_string(),
1111                    digest: None,
1112                }))),
1113            });
1114        }
1115        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1116            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1117            unlabeled: None,
1118            arguments,
1119            digest: None,
1120            non_code_meta: Default::default(),
1121        })));
1122
1123        // Look up existing sketch.
1124        let sketch_id = sketch;
1125        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1126            msg: format!("Sketch not found: {sketch:?}"),
1127        })?;
1128        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1129            return Err(Error {
1130                msg: format!("Object is not a sketch: {sketch_object:?}"),
1131            });
1132        };
1133        // Add the line to the AST of the sketch block.
1134        let mut new_ast = self.program.ast.clone();
1135        let (sketch_block_range, _) = self.mutate_ast(
1136            &mut new_ast,
1137            sketch_id,
1138            AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1139        )?;
1140        // Convert to string source to create real source ranges.
1141        let new_source = source_from_ast(&new_ast);
1142        // Parse the new KCL source.
1143        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1144        if !errors.is_empty() {
1145            return Err(Error {
1146                msg: format!("Error parsing KCL source after adding line: {errors:?}"),
1147            });
1148        }
1149        let Some(new_program) = new_program else {
1150            return Err(Error {
1151                msg: "No AST produced after adding line".to_string(),
1152            });
1153        };
1154        let line_source_range =
1155            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1156                msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
1157            })?;
1158        #[cfg(not(feature = "artifact-graph"))]
1159        let _ = line_source_range;
1160
1161        // Make sure to only set this if there are no errors.
1162        self.program = new_program.clone();
1163
1164        // Truncate after the sketch block for mock execution.
1165        let mut truncated_program = new_program;
1166        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1167
1168        // Execute.
1169        let outcome = ctx
1170            .run_mock(
1171                &truncated_program,
1172                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1173            )
1174            .await
1175            .map_err(|err| {
1176                // TODO: sketch-api: Yeah, this needs to change. We need to
1177                // return the full error.
1178                Error {
1179                    msg: err.error.message().to_owned(),
1180                }
1181            })?;
1182
1183        #[cfg(not(feature = "artifact-graph"))]
1184        let new_object_ids = Vec::new();
1185        #[cfg(feature = "artifact-graph")]
1186        let new_object_ids = {
1187            let segment_id = outcome
1188                .source_range_to_object
1189                .get(&line_source_range)
1190                .copied()
1191                .ok_or_else(|| Error {
1192                    msg: format!("Source range of line not found: {line_source_range:?}"),
1193                })?;
1194            let segment_object = outcome.scene_object_by_id(segment_id).ok_or_else(|| Error {
1195                msg: format!("Segment not found: {segment_id:?}"),
1196            })?;
1197            let ObjectKind::Segment { segment } = &segment_object.kind else {
1198                return Err(Error {
1199                    msg: format!("Object is not a segment: {segment_object:?}"),
1200                });
1201            };
1202            let Segment::Line(line) = segment else {
1203                return Err(Error {
1204                    msg: format!("Segment is not a line: {segment:?}"),
1205                });
1206            };
1207            vec![line.start, line.end, segment_id]
1208        };
1209        let src_delta = SourceDelta { text: new_source };
1210        // Uses .no_freedom_analysis() so freedom_analysis: false
1211        let outcome = self.update_state_after_exec(outcome, false);
1212        let scene_graph_delta = SceneGraphDelta {
1213            new_graph: self.scene_graph.clone(),
1214            invalidates_ids: false,
1215            new_objects: new_object_ids,
1216            exec_outcome: outcome,
1217        };
1218        Ok((src_delta, scene_graph_delta))
1219    }
1220
1221    async fn add_arc(
1222        &mut self,
1223        ctx: &ExecutorContext,
1224        sketch: ObjectId,
1225        ctor: ArcCtor,
1226    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1227        // Create updated KCL source from args.
1228        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1229        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1230        let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1231        let mut arguments = vec![
1232            ast::LabeledArg {
1233                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1234                arg: start_ast,
1235            },
1236            ast::LabeledArg {
1237                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1238                arg: end_ast,
1239            },
1240            ast::LabeledArg {
1241                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1242                arg: center_ast,
1243            },
1244        ];
1245        // Add construction kwarg if construction is Some(true)
1246        if ctor.construction == Some(true) {
1247            arguments.push(ast::LabeledArg {
1248                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1249                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1250                    value: ast::LiteralValue::Bool(true),
1251                    raw: "true".to_string(),
1252                    digest: None,
1253                }))),
1254            });
1255        }
1256        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1257            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1258            unlabeled: None,
1259            arguments,
1260            digest: None,
1261            non_code_meta: Default::default(),
1262        })));
1263
1264        // Look up existing sketch.
1265        let sketch_id = sketch;
1266        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1267            msg: format!("Sketch not found: {sketch:?}"),
1268        })?;
1269        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1270            return Err(Error {
1271                msg: format!("Object is not a sketch: {sketch_object:?}"),
1272            });
1273        };
1274        // Add the arc to the AST of the sketch block.
1275        let mut new_ast = self.program.ast.clone();
1276        let (sketch_block_range, _) = self.mutate_ast(
1277            &mut new_ast,
1278            sketch_id,
1279            AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1280        )?;
1281        // Convert to string source to create real source ranges.
1282        let new_source = source_from_ast(&new_ast);
1283        // Parse the new KCL source.
1284        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1285        if !errors.is_empty() {
1286            return Err(Error {
1287                msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
1288            });
1289        }
1290        let Some(new_program) = new_program else {
1291            return Err(Error {
1292                msg: "No AST produced after adding arc".to_string(),
1293            });
1294        };
1295        let arc_source_range =
1296            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1297                msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
1298            })?;
1299        #[cfg(not(feature = "artifact-graph"))]
1300        let _ = arc_source_range;
1301
1302        // Make sure to only set this if there are no errors.
1303        self.program = new_program.clone();
1304
1305        // Truncate after the sketch block for mock execution.
1306        let mut truncated_program = new_program;
1307        self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1308
1309        // Execute.
1310        let outcome = ctx
1311            .run_mock(
1312                &truncated_program,
1313                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1314            )
1315            .await
1316            .map_err(|err| {
1317                // TODO: sketch-api: Yeah, this needs to change. We need to
1318                // return the full error.
1319                Error {
1320                    msg: err.error.message().to_owned(),
1321                }
1322            })?;
1323
1324        #[cfg(not(feature = "artifact-graph"))]
1325        let new_object_ids = Vec::new();
1326        #[cfg(feature = "artifact-graph")]
1327        let new_object_ids = {
1328            let segment_id = outcome
1329                .source_range_to_object
1330                .get(&arc_source_range)
1331                .copied()
1332                .ok_or_else(|| Error {
1333                    msg: format!("Source range of arc not found: {arc_source_range:?}"),
1334                })?;
1335            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1336                msg: format!("Segment not found: {segment_id:?}"),
1337            })?;
1338            let ObjectKind::Segment { segment } = &segment_object.kind else {
1339                return Err(Error {
1340                    msg: format!("Object is not a segment: {segment_object:?}"),
1341                });
1342            };
1343            let Segment::Arc(arc) = segment else {
1344                return Err(Error {
1345                    msg: format!("Segment is not an arc: {segment:?}"),
1346                });
1347            };
1348            vec![arc.start, arc.end, arc.center, segment_id]
1349        };
1350        let src_delta = SourceDelta { text: new_source };
1351        // Uses .no_freedom_analysis() so freedom_analysis: false
1352        let outcome = self.update_state_after_exec(outcome, false);
1353        let scene_graph_delta = SceneGraphDelta {
1354            new_graph: self.scene_graph.clone(),
1355            invalidates_ids: false,
1356            new_objects: new_object_ids,
1357            exec_outcome: outcome,
1358        };
1359        Ok((src_delta, scene_graph_delta))
1360    }
1361
1362    fn edit_point(
1363        &mut self,
1364        new_ast: &mut ast::Node<ast::Program>,
1365        sketch: ObjectId,
1366        point: ObjectId,
1367        ctor: PointCtor,
1368    ) -> api::Result<()> {
1369        // Create updated KCL source from args.
1370        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1371
1372        // Look up existing sketch.
1373        let sketch_id = sketch;
1374        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1375            msg: format!("Sketch not found: {sketch:?}"),
1376        })?;
1377        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1378            return Err(Error {
1379                msg: format!("Object is not a sketch: {sketch_object:?}"),
1380            });
1381        };
1382        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1383            msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1384        })?;
1385        // Look up existing point.
1386        let point_id = point;
1387        let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1388            msg: format!("Point not found in scene graph: point={point:?}"),
1389        })?;
1390        let ObjectKind::Segment {
1391            segment: Segment::Point(point),
1392        } = &point_object.kind
1393        else {
1394            return Err(Error {
1395                msg: format!("Object is not a point segment: {point_object:?}"),
1396            });
1397        };
1398
1399        // If the point is part of a line or arc, edit the line/arc instead.
1400        if let Some(owner_id) = point.owner {
1401            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1402                msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1403            })?;
1404            let ObjectKind::Segment { segment } = &owner_object.kind else {
1405                return Err(Error {
1406                    msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1407                });
1408            };
1409
1410            // Handle Line owner
1411            if let Segment::Line(line) = segment {
1412                let SegmentCtor::Line(line_ctor) = &line.ctor else {
1413                    return Err(Error {
1414                        msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1415                    });
1416                };
1417                let mut line_ctor = line_ctor.clone();
1418                // Which end of the line is this point?
1419                if line.start == point_id {
1420                    line_ctor.start = ctor.position;
1421                } else if line.end == point_id {
1422                    line_ctor.end = ctor.position;
1423                } else {
1424                    return Err(Error {
1425                        msg: format!(
1426                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1427                        ),
1428                    });
1429                }
1430                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1431            }
1432
1433            // Handle Arc owner
1434            if let Segment::Arc(arc) = segment {
1435                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1436                    return Err(Error {
1437                        msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1438                    });
1439                };
1440                let mut arc_ctor = arc_ctor.clone();
1441                // Which point of the arc is this? (center, start, or end)
1442                if arc.center == point_id {
1443                    arc_ctor.center = ctor.position;
1444                } else if arc.start == point_id {
1445                    arc_ctor.start = ctor.position;
1446                } else if arc.end == point_id {
1447                    arc_ctor.end = ctor.position;
1448                } else {
1449                    return Err(Error {
1450                        msg: format!(
1451                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1452                        ),
1453                    });
1454                }
1455                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1456            }
1457
1458            // If owner is neither Line nor Arc, allow editing the point directly
1459            // (fall through to the point editing logic below)
1460        }
1461
1462        // Modify the point AST.
1463        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1464        Ok(())
1465    }
1466
1467    fn edit_line(
1468        &mut self,
1469        new_ast: &mut ast::Node<ast::Program>,
1470        sketch: ObjectId,
1471        line: ObjectId,
1472        ctor: LineCtor,
1473    ) -> api::Result<()> {
1474        // Create updated KCL source from args.
1475        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1476        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1477
1478        // Look up existing sketch.
1479        let sketch_id = sketch;
1480        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1481            msg: format!("Sketch not found: {sketch:?}"),
1482        })?;
1483        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1484            return Err(Error {
1485                msg: format!("Object is not a sketch: {sketch_object:?}"),
1486            });
1487        };
1488        sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1489            msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1490        })?;
1491        // Look up existing line.
1492        let line_id = line;
1493        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1494            msg: format!("Line not found in scene graph: line={line:?}"),
1495        })?;
1496        let ObjectKind::Segment { .. } = &line_object.kind else {
1497            return Err(Error {
1498                msg: format!("Object is not a segment: {line_object:?}"),
1499            });
1500        };
1501
1502        // Modify the line AST.
1503        self.mutate_ast(
1504            new_ast,
1505            line_id,
1506            AstMutateCommand::EditLine {
1507                start: new_start_ast,
1508                end: new_end_ast,
1509                construction: ctor.construction,
1510            },
1511        )?;
1512        Ok(())
1513    }
1514
1515    fn edit_arc(
1516        &mut self,
1517        new_ast: &mut ast::Node<ast::Program>,
1518        sketch: ObjectId,
1519        arc: ObjectId,
1520        ctor: ArcCtor,
1521    ) -> api::Result<()> {
1522        // Create updated KCL source from args.
1523        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1524        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1525        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1526
1527        // Look up existing sketch.
1528        let sketch_id = sketch;
1529        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1530            msg: format!("Sketch not found: {sketch:?}"),
1531        })?;
1532        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1533            return Err(Error {
1534                msg: format!("Object is not a sketch: {sketch_object:?}"),
1535            });
1536        };
1537        sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1538            msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1539        })?;
1540        // Look up existing arc.
1541        let arc_id = arc;
1542        let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1543            msg: format!("Arc not found in scene graph: arc={arc:?}"),
1544        })?;
1545        let ObjectKind::Segment { .. } = &arc_object.kind else {
1546            return Err(Error {
1547                msg: format!("Object is not a segment: {arc_object:?}"),
1548            });
1549        };
1550
1551        // Modify the arc AST.
1552        self.mutate_ast(
1553            new_ast,
1554            arc_id,
1555            AstMutateCommand::EditArc {
1556                start: new_start_ast,
1557                end: new_end_ast,
1558                center: new_center_ast,
1559                construction: ctor.construction,
1560            },
1561        )?;
1562        Ok(())
1563    }
1564
1565    fn delete_segment(
1566        &mut self,
1567        new_ast: &mut ast::Node<ast::Program>,
1568        sketch: ObjectId,
1569        segment_id: ObjectId,
1570    ) -> api::Result<()> {
1571        // Look up existing sketch.
1572        let sketch_id = sketch;
1573        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1574            msg: format!("Sketch not found: {sketch:?}"),
1575        })?;
1576        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1577            return Err(Error {
1578                msg: format!("Object is not a sketch: {sketch_object:?}"),
1579            });
1580        };
1581        sketch
1582            .segments
1583            .iter()
1584            .find(|o| **o == segment_id)
1585            .ok_or_else(|| Error {
1586                msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1587            })?;
1588        // Look up existing segment.
1589        let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1590            msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1591        })?;
1592        let ObjectKind::Segment { .. } = &segment_object.kind else {
1593            return Err(Error {
1594                msg: format!("Object is not a segment: {segment_object:?}"),
1595            });
1596        };
1597
1598        // Modify the AST to remove the segment.
1599        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1600        Ok(())
1601    }
1602
1603    fn delete_constraint(
1604        &mut self,
1605        new_ast: &mut ast::Node<ast::Program>,
1606        sketch: ObjectId,
1607        constraint_id: ObjectId,
1608    ) -> api::Result<()> {
1609        // Look up existing sketch.
1610        let sketch_id = sketch;
1611        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1612            msg: format!("Sketch not found: {sketch:?}"),
1613        })?;
1614        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1615            return Err(Error {
1616                msg: format!("Object is not a sketch: {sketch_object:?}"),
1617            });
1618        };
1619        sketch
1620            .constraints
1621            .iter()
1622            .find(|o| **o == constraint_id)
1623            .ok_or_else(|| Error {
1624                msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1625            })?;
1626        // Look up existing constraint.
1627        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1628            msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1629        })?;
1630        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1631            return Err(Error {
1632                msg: format!("Object is not a constraint: {constraint_object:?}"),
1633            });
1634        };
1635
1636        // Modify the AST to remove the constraint.
1637        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1638        Ok(())
1639    }
1640
1641    async fn execute_after_edit(
1642        &mut self,
1643        ctx: &ExecutorContext,
1644        sketch: ObjectId,
1645        segment_ids_edited: AhashIndexSet<ObjectId>,
1646        edit_kind: EditDeleteKind,
1647        new_ast: &mut ast::Node<ast::Program>,
1648    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1649        // Convert to string source to create real source ranges.
1650        let new_source = source_from_ast(new_ast);
1651        // Parse the new KCL source.
1652        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1653        if !errors.is_empty() {
1654            return Err(Error {
1655                msg: format!("Error parsing KCL source after editing: {errors:?}"),
1656            });
1657        }
1658        let Some(new_program) = new_program else {
1659            return Err(Error {
1660                msg: "No AST produced after editing".to_string(),
1661            });
1662        };
1663
1664        // TODO: sketch-api: make sure to only set this if there are no errors.
1665        self.program = new_program.clone();
1666
1667        // Truncate after the sketch block for mock execution.
1668        let is_delete = edit_kind.is_delete();
1669        let truncated_program = {
1670            let mut truncated_program = new_program;
1671            self.only_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
1672            truncated_program
1673        };
1674
1675        #[cfg(not(feature = "artifact-graph"))]
1676        drop(segment_ids_edited);
1677
1678        // Execute.
1679        let mock_config = MockConfig {
1680            sketch_block_id: Some(sketch),
1681            freedom_analysis: is_delete,
1682            #[cfg(feature = "artifact-graph")]
1683            segment_ids_edited: segment_ids_edited.clone(),
1684            ..Default::default()
1685        };
1686        let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
1687            // TODO: sketch-api: Yeah, this needs to change. We need to
1688            // return the full error.
1689            Error {
1690                msg: err.error.message().to_owned(),
1691            }
1692        })?;
1693
1694        // Uses freedom_analysis: is_delete
1695        let outcome = self.update_state_after_exec(outcome, is_delete);
1696
1697        #[cfg(feature = "artifact-graph")]
1698        let new_source = {
1699            // Feed back sketch var solutions into the source.
1700            //
1701            // The interpreter is returning all var solutions from the sketch
1702            // block we're editing.
1703            let mut new_ast = self.program.ast.clone();
1704            for (var_range, value) in &outcome.var_solutions {
1705                let rounded = value.round(3);
1706                mutate_ast_node_by_source_range(
1707                    &mut new_ast,
1708                    *var_range,
1709                    AstMutateCommand::EditVarInitialValue { value: rounded },
1710                )?;
1711            }
1712            source_from_ast(&new_ast)
1713        };
1714
1715        let src_delta = SourceDelta { text: new_source };
1716        let scene_graph_delta = SceneGraphDelta {
1717            new_graph: self.scene_graph.clone(),
1718            invalidates_ids: is_delete,
1719            new_objects: Vec::new(),
1720            exec_outcome: outcome,
1721        };
1722        Ok((src_delta, scene_graph_delta))
1723    }
1724
1725    async fn execute_after_delete_sketch(
1726        &mut self,
1727        ctx: &ExecutorContext,
1728        new_ast: &mut ast::Node<ast::Program>,
1729    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1730        // Convert to string source to create real source ranges.
1731        let new_source = source_from_ast(new_ast);
1732        // Parse the new KCL source.
1733        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1734        if !errors.is_empty() {
1735            return Err(Error {
1736                msg: format!("Error parsing KCL source after editing: {errors:?}"),
1737            });
1738        }
1739        let Some(new_program) = new_program else {
1740            return Err(Error {
1741                msg: "No AST produced after editing".to_string(),
1742            });
1743        };
1744
1745        // Make sure to only set this if there are no errors.
1746        self.program = new_program.clone();
1747
1748        // We deleted the entire sketch block. It doesn't make sense to truncate
1749        // and execute only the sketch block. We execute the whole program with
1750        // a real engine.
1751
1752        // Execute.
1753        let outcome = ctx.run_with_caching(new_program).await.map_err(|err| {
1754            // TODO: sketch-api: Yeah, this needs to change. We need to
1755            // return the full error.
1756            Error {
1757                msg: err.error.message().to_owned(),
1758            }
1759        })?;
1760        let freedom_analysis_ran = true;
1761
1762        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
1763
1764        let src_delta = SourceDelta { text: new_source };
1765        let scene_graph_delta = SceneGraphDelta {
1766            new_graph: self.scene_graph.clone(),
1767            invalidates_ids: true,
1768            new_objects: Vec::new(),
1769            exec_outcome: outcome,
1770        };
1771        Ok((src_delta, scene_graph_delta))
1772    }
1773
1774    /// Map a point object id into an AST reference expression for use in
1775    /// constraints. If the point is owned by a segment (line or arc), we
1776    /// reference the appropriate property on that segment (e.g. `line1.start`,
1777    /// `arc1.center`). Otherwise we reference the point directly.
1778    fn point_id_to_ast_reference(
1779        &self,
1780        point_id: ObjectId,
1781        new_ast: &mut ast::Node<ast::Program>,
1782    ) -> api::Result<ast::Expr> {
1783        let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1784            msg: format!("Point not found: {point_id:?}"),
1785        })?;
1786        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
1787            return Err(Error {
1788                msg: format!("Object is not a segment: {point_object:?}"),
1789            });
1790        };
1791        let Segment::Point(point) = point_segment else {
1792            return Err(Error {
1793                msg: format!("Only points are currently supported: {point_object:?}"),
1794            });
1795        };
1796
1797        if let Some(owner_id) = point.owner {
1798            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1799                msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
1800            })?;
1801            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
1802                return Err(Error {
1803                    msg: format!("Owner of point is not a segment: {owner_object:?}"),
1804                });
1805            };
1806
1807            match owner_segment {
1808                Segment::Line(line) => {
1809                    let property = if line.start == point_id {
1810                        LINE_PROPERTY_START
1811                    } else if line.end == point_id {
1812                        LINE_PROPERTY_END
1813                    } else {
1814                        return Err(Error {
1815                            msg: format!(
1816                                "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1817                            ),
1818                        });
1819                    };
1820                    get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
1821                }
1822                Segment::Arc(arc) => {
1823                    let property = if arc.start == point_id {
1824                        ARC_PROPERTY_START
1825                    } else if arc.end == point_id {
1826                        ARC_PROPERTY_END
1827                    } else if arc.center == point_id {
1828                        ARC_PROPERTY_CENTER
1829                    } else {
1830                        return Err(Error {
1831                            msg: format!(
1832                                "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1833                            ),
1834                        });
1835                    };
1836                    get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
1837                }
1838                _ => Err(Error {
1839                    msg: format!(
1840                        "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
1841                    ),
1842                }),
1843            }
1844        } else {
1845            // Standalone point.
1846            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
1847        }
1848    }
1849
1850    async fn add_coincident(
1851        &mut self,
1852        sketch: ObjectId,
1853        coincident: Coincident,
1854        new_ast: &mut ast::Node<ast::Program>,
1855    ) -> api::Result<SourceRange> {
1856        let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1857            return Err(Error {
1858                msg: format!(
1859                    "Coincident constraint must have exactly 2 segments, got {}",
1860                    coincident.segments.len()
1861                ),
1862            });
1863        };
1864        let sketch_id = sketch;
1865
1866        // Get AST reference for first object (point or segment)
1867        let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1868            msg: format!("Object not found: {seg0_id:?}"),
1869        })?;
1870        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1871            return Err(Error {
1872                msg: format!("Object is not a segment: {seg0_object:?}"),
1873            });
1874        };
1875        let seg0_ast = match seg0_segment {
1876            Segment::Point(_) => {
1877                // Use the helper function which supports both Line and Arc owners
1878                self.point_id_to_ast_reference(seg0_id, new_ast)?
1879            }
1880            Segment::Line(_) => {
1881                // Reference the segment directly (for point-segment coincident)
1882                get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1883            }
1884            Segment::Arc(_) | Segment::Circle(_) => {
1885                // Reference the segment directly (for point-arc coincident)
1886                get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
1887            }
1888        };
1889
1890        // Get AST reference for second object (point or segment)
1891        let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1892            msg: format!("Object not found: {seg1_id:?}"),
1893        })?;
1894        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1895            return Err(Error {
1896                msg: format!("Object is not a segment: {seg1_object:?}"),
1897            });
1898        };
1899        let seg1_ast = match seg1_segment {
1900            Segment::Point(_) => {
1901                // Use the helper function which supports both Line and Arc owners
1902                self.point_id_to_ast_reference(seg1_id, new_ast)?
1903            }
1904            Segment::Line(_) => {
1905                // Reference the segment directly (for point-segment coincident)
1906                get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1907            }
1908            Segment::Arc(_) | Segment::Circle(_) => {
1909                // Reference the segment directly (for point-arc coincident)
1910                get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
1911            }
1912        };
1913
1914        // Create the coincident() call using shared helper.
1915        let coincident_ast = create_coincident_ast(seg0_ast, seg1_ast);
1916
1917        // Add the line to the AST of the sketch block.
1918        let (sketch_block_range, _) = self.mutate_ast(
1919            new_ast,
1920            sketch_id,
1921            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1922        )?;
1923        Ok(sketch_block_range)
1924    }
1925
1926    async fn add_distance(
1927        &mut self,
1928        sketch: ObjectId,
1929        distance: Distance,
1930        new_ast: &mut ast::Node<ast::Program>,
1931    ) -> api::Result<SourceRange> {
1932        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1933            return Err(Error {
1934                msg: format!(
1935                    "Distance constraint must have exactly 2 points, got {}",
1936                    distance.points.len()
1937                ),
1938            });
1939        };
1940        let sketch_id = sketch;
1941
1942        // Map the runtime objects back to variable names.
1943        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1944        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1945
1946        // Create the distance() call.
1947        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1948            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1949            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1950                ast::ArrayExpression {
1951                    elements: vec![pt0_ast, pt1_ast],
1952                    digest: None,
1953                    non_code_meta: Default::default(),
1954                },
1955            )))),
1956            arguments: Default::default(),
1957            digest: None,
1958            non_code_meta: Default::default(),
1959        })));
1960        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1961            left: distance_call_ast,
1962            operator: ast::BinaryOperator::Eq,
1963            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1964                value: ast::LiteralValue::Number {
1965                    value: distance.distance.value,
1966                    suffix: distance.distance.units,
1967                },
1968                raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1969                    msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1970                })?,
1971                digest: None,
1972            }))),
1973            digest: None,
1974        })));
1975
1976        // Add the line to the AST of the sketch block.
1977        let (sketch_block_range, _) = self.mutate_ast(
1978            new_ast,
1979            sketch_id,
1980            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1981        )?;
1982        Ok(sketch_block_range)
1983    }
1984
1985    async fn add_radius(
1986        &mut self,
1987        sketch: ObjectId,
1988        radius: Radius,
1989        new_ast: &mut ast::Node<ast::Program>,
1990    ) -> api::Result<SourceRange> {
1991        let params = ArcSizeConstraintParams {
1992            points: vec![radius.arc],
1993            function_name: RADIUS_FN,
1994            value: radius.radius.value,
1995            units: radius.radius.units,
1996            constraint_type_name: "Radius",
1997        };
1998        self.add_arc_size_constraint(sketch, params, new_ast).await
1999    }
2000
2001    async fn add_diameter(
2002        &mut self,
2003        sketch: ObjectId,
2004        diameter: Diameter,
2005        new_ast: &mut ast::Node<ast::Program>,
2006    ) -> api::Result<SourceRange> {
2007        let params = ArcSizeConstraintParams {
2008            points: vec![diameter.arc],
2009            function_name: DIAMETER_FN,
2010            value: diameter.diameter.value,
2011            units: diameter.diameter.units,
2012            constraint_type_name: "Diameter",
2013        };
2014        self.add_arc_size_constraint(sketch, params, new_ast).await
2015    }
2016
2017    async fn add_arc_size_constraint(
2018        &mut self,
2019        sketch: ObjectId,
2020        params: ArcSizeConstraintParams,
2021        new_ast: &mut ast::Node<ast::Program>,
2022    ) -> api::Result<SourceRange> {
2023        let sketch_id = sketch;
2024
2025        // Constraint must have exactly 1 argument (arc segment)
2026        if params.points.len() != 1 {
2027            return Err(Error {
2028                msg: format!(
2029                    "{} constraint must have exactly 1 argument (an arc segment), got {}",
2030                    params.constraint_type_name,
2031                    params.points.len()
2032                ),
2033            });
2034        }
2035
2036        let arc_id = params.points[0];
2037        let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
2038            msg: format!("Arc segment not found: {arc_id:?}"),
2039        })?;
2040        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
2041            return Err(Error {
2042                msg: format!("Object is not a segment: {arc_object:?}"),
2043            });
2044        };
2045        let Segment::Arc(_) = arc_segment else {
2046            return Err(Error {
2047                msg: format!(
2048                    "{} constraint argument must be an arc segment, got: {arc_segment:?}",
2049                    params.constraint_type_name
2050                ),
2051            });
2052        };
2053        // Reference the arc segment directly
2054        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, "arc", None)?;
2055
2056        // Create the function call.
2057        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2058            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
2059            unlabeled: Some(arc_ast),
2060            arguments: Default::default(),
2061            digest: None,
2062            non_code_meta: Default::default(),
2063        })));
2064        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2065            left: call_ast,
2066            operator: ast::BinaryOperator::Eq,
2067            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2068                value: ast::LiteralValue::Number {
2069                    value: params.value,
2070                    suffix: params.units,
2071                },
2072                raw: format_number_literal(params.value, params.units).map_err(|_| Error {
2073                    msg: format!("Could not format numeric suffix: {:?}", params.units),
2074                })?,
2075                digest: None,
2076            }))),
2077            digest: None,
2078        })));
2079
2080        // Add the line to the AST of the sketch block.
2081        let (sketch_block_range, _) = self.mutate_ast(
2082            new_ast,
2083            sketch_id,
2084            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
2085        )?;
2086        Ok(sketch_block_range)
2087    }
2088
2089    async fn add_horizontal_distance(
2090        &mut self,
2091        sketch: ObjectId,
2092        distance: Distance,
2093        new_ast: &mut ast::Node<ast::Program>,
2094    ) -> api::Result<SourceRange> {
2095        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2096            return Err(Error {
2097                msg: format!(
2098                    "Horizontal distance constraint must have exactly 2 points, got {}",
2099                    distance.points.len()
2100                ),
2101            });
2102        };
2103        let sketch_id = sketch;
2104
2105        // Map the runtime objects back to variable names.
2106        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2107        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2108
2109        // Create the horizontalDistance() call.
2110        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2111            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
2112            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2113                ast::ArrayExpression {
2114                    elements: vec![pt0_ast, pt1_ast],
2115                    digest: None,
2116                    non_code_meta: Default::default(),
2117                },
2118            )))),
2119            arguments: Default::default(),
2120            digest: None,
2121            non_code_meta: Default::default(),
2122        })));
2123        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2124            left: distance_call_ast,
2125            operator: ast::BinaryOperator::Eq,
2126            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2127                value: ast::LiteralValue::Number {
2128                    value: distance.distance.value,
2129                    suffix: distance.distance.units,
2130                },
2131                raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2132                    msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2133                })?,
2134                digest: None,
2135            }))),
2136            digest: None,
2137        })));
2138
2139        // Add the line to the AST of the sketch block.
2140        let (sketch_block_range, _) = self.mutate_ast(
2141            new_ast,
2142            sketch_id,
2143            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2144        )?;
2145        Ok(sketch_block_range)
2146    }
2147
2148    async fn add_vertical_distance(
2149        &mut self,
2150        sketch: ObjectId,
2151        distance: Distance,
2152        new_ast: &mut ast::Node<ast::Program>,
2153    ) -> api::Result<SourceRange> {
2154        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2155            return Err(Error {
2156                msg: format!(
2157                    "Vertical distance constraint must have exactly 2 points, got {}",
2158                    distance.points.len()
2159                ),
2160            });
2161        };
2162        let sketch_id = sketch;
2163
2164        // Map the runtime objects back to variable names.
2165        let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2166        let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2167
2168        // Create the verticalDistance() call.
2169        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2170            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
2171            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2172                ast::ArrayExpression {
2173                    elements: vec![pt0_ast, pt1_ast],
2174                    digest: None,
2175                    non_code_meta: Default::default(),
2176                },
2177            )))),
2178            arguments: Default::default(),
2179            digest: None,
2180            non_code_meta: Default::default(),
2181        })));
2182        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2183            left: distance_call_ast,
2184            operator: ast::BinaryOperator::Eq,
2185            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2186                value: ast::LiteralValue::Number {
2187                    value: distance.distance.value,
2188                    suffix: distance.distance.units,
2189                },
2190                raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2191                    msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2192                })?,
2193                digest: None,
2194            }))),
2195            digest: None,
2196        })));
2197
2198        // Add the line to the AST of the sketch block.
2199        let (sketch_block_range, _) = self.mutate_ast(
2200            new_ast,
2201            sketch_id,
2202            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2203        )?;
2204        Ok(sketch_block_range)
2205    }
2206
2207    async fn add_horizontal(
2208        &mut self,
2209        sketch: ObjectId,
2210        horizontal: Horizontal,
2211        new_ast: &mut ast::Node<ast::Program>,
2212    ) -> api::Result<SourceRange> {
2213        let sketch_id = sketch;
2214
2215        // Map the runtime objects back to variable names.
2216        let line_id = horizontal.line;
2217        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2218            msg: format!("Line not found: {line_id:?}"),
2219        })?;
2220        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2221            return Err(Error {
2222                msg: format!("Object is not a segment: {line_object:?}"),
2223            });
2224        };
2225        let Segment::Line(_) = line_segment else {
2226            return Err(Error {
2227                msg: format!("Only lines can be made horizontal: {line_object:?}"),
2228            });
2229        };
2230        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2231
2232        // Create the horizontal() call using shared helper.
2233        let horizontal_ast = create_horizontal_ast(line_ast);
2234
2235        // Add the line to the AST of the sketch block.
2236        let (sketch_block_range, _) = self.mutate_ast(
2237            new_ast,
2238            sketch_id,
2239            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
2240        )?;
2241        Ok(sketch_block_range)
2242    }
2243
2244    async fn add_lines_equal_length(
2245        &mut self,
2246        sketch: ObjectId,
2247        lines_equal_length: LinesEqualLength,
2248        new_ast: &mut ast::Node<ast::Program>,
2249    ) -> api::Result<SourceRange> {
2250        let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
2251            return Err(Error {
2252                msg: format!(
2253                    "Lines equal length constraint must have exactly 2 lines, got {}",
2254                    lines_equal_length.lines.len()
2255                ),
2256            });
2257        };
2258
2259        let sketch_id = sketch;
2260
2261        // Map the runtime objects back to variable names.
2262        let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2263            msg: format!("Line not found: {line0_id:?}"),
2264        })?;
2265        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2266            return Err(Error {
2267                msg: format!("Object is not a segment: {line0_object:?}"),
2268            });
2269        };
2270        let Segment::Line(_) = line0_segment else {
2271            return Err(Error {
2272                msg: format!("Only lines can be made equal length: {line0_object:?}"),
2273            });
2274        };
2275        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2276
2277        let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2278            msg: format!("Line not found: {line1_id:?}"),
2279        })?;
2280        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2281            return Err(Error {
2282                msg: format!("Object is not a segment: {line1_object:?}"),
2283            });
2284        };
2285        let Segment::Line(_) = line1_segment else {
2286            return Err(Error {
2287                msg: format!("Only lines can be made equal length: {line1_object:?}"),
2288            });
2289        };
2290        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2291
2292        // Create the equalLength() call using shared helper.
2293        let equal_length_ast = create_equal_length_ast(line0_ast, line1_ast);
2294
2295        // Add the constraint to the AST of the sketch block.
2296        let (sketch_block_range, _) = self.mutate_ast(
2297            new_ast,
2298            sketch_id,
2299            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
2300        )?;
2301        Ok(sketch_block_range)
2302    }
2303
2304    async fn add_parallel(
2305        &mut self,
2306        sketch: ObjectId,
2307        parallel: Parallel,
2308        new_ast: &mut ast::Node<ast::Program>,
2309    ) -> api::Result<SourceRange> {
2310        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
2311            .await
2312    }
2313
2314    async fn add_perpendicular(
2315        &mut self,
2316        sketch: ObjectId,
2317        perpendicular: Perpendicular,
2318        new_ast: &mut ast::Node<ast::Program>,
2319    ) -> api::Result<SourceRange> {
2320        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
2321            .await
2322    }
2323
2324    async fn add_lines_at_angle_constraint(
2325        &mut self,
2326        sketch: ObjectId,
2327        angle_kind: LinesAtAngleKind,
2328        lines: Vec<ObjectId>,
2329        new_ast: &mut ast::Node<ast::Program>,
2330    ) -> api::Result<SourceRange> {
2331        let &[line0_id, line1_id] = lines.as_slice() else {
2332            return Err(Error {
2333                msg: format!(
2334                    "{} constraint must have exactly 2 lines, got {}",
2335                    angle_kind.to_function_name(),
2336                    lines.len()
2337                ),
2338            });
2339        };
2340
2341        let sketch_id = sketch;
2342
2343        // Map the runtime objects back to variable names.
2344        let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2345            msg: format!("Line not found: {line0_id:?}"),
2346        })?;
2347        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2348            return Err(Error {
2349                msg: format!("Object is not a segment: {line0_object:?}"),
2350            });
2351        };
2352        let Segment::Line(_) = line0_segment else {
2353            return Err(Error {
2354                msg: format!(
2355                    "Only lines can be made {}: {line0_object:?}",
2356                    angle_kind.to_function_name()
2357                ),
2358            });
2359        };
2360        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2361
2362        let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2363            msg: format!("Line not found: {line1_id:?}"),
2364        })?;
2365        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2366            return Err(Error {
2367                msg: format!("Object is not a segment: {line1_object:?}"),
2368            });
2369        };
2370        let Segment::Line(_) = line1_segment else {
2371            return Err(Error {
2372                msg: format!(
2373                    "Only lines can be made {}: {line1_object:?}",
2374                    angle_kind.to_function_name()
2375                ),
2376            });
2377        };
2378        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2379
2380        // Create the parallel() or perpendicular() call.
2381        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2382            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
2383            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2384                ast::ArrayExpression {
2385                    elements: vec![line0_ast, line1_ast],
2386                    digest: None,
2387                    non_code_meta: Default::default(),
2388                },
2389            )))),
2390            arguments: Default::default(),
2391            digest: None,
2392            non_code_meta: Default::default(),
2393        })));
2394
2395        // Add the constraint to the AST of the sketch block.
2396        let (sketch_block_range, _) = self.mutate_ast(
2397            new_ast,
2398            sketch_id,
2399            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
2400        )?;
2401        Ok(sketch_block_range)
2402    }
2403
2404    async fn add_vertical(
2405        &mut self,
2406        sketch: ObjectId,
2407        vertical: Vertical,
2408        new_ast: &mut ast::Node<ast::Program>,
2409    ) -> api::Result<SourceRange> {
2410        let sketch_id = sketch;
2411
2412        // Map the runtime objects back to variable names.
2413        let line_id = vertical.line;
2414        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2415            msg: format!("Line not found: {line_id:?}"),
2416        })?;
2417        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2418            return Err(Error {
2419                msg: format!("Object is not a segment: {line_object:?}"),
2420            });
2421        };
2422        let Segment::Line(_) = line_segment else {
2423            return Err(Error {
2424                msg: format!("Only lines can be made vertical: {line_object:?}"),
2425            });
2426        };
2427        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2428
2429        // Create the vertical() call using shared helper.
2430        let vertical_ast = create_vertical_ast(line_ast);
2431
2432        // Add the line to the AST of the sketch block.
2433        let (sketch_block_range, _) = self.mutate_ast(
2434            new_ast,
2435            sketch_id,
2436            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
2437        )?;
2438        Ok(sketch_block_range)
2439    }
2440
2441    async fn execute_after_add_constraint(
2442        &mut self,
2443        ctx: &ExecutorContext,
2444        sketch_id: ObjectId,
2445        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
2446        new_ast: &mut ast::Node<ast::Program>,
2447    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2448        // Convert to string source to create real source ranges.
2449        let new_source = source_from_ast(new_ast);
2450        // Parse the new KCL source.
2451        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2452        if !errors.is_empty() {
2453            return Err(Error {
2454                msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
2455            });
2456        }
2457        let Some(new_program) = new_program else {
2458            return Err(Error {
2459                msg: "No AST produced after adding constraint".to_string(),
2460            });
2461        };
2462        #[cfg(feature = "artifact-graph")]
2463        let constraint_source_range =
2464            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
2465                msg: format!(
2466                    "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
2467                ),
2468            })?;
2469
2470        // Truncate after the sketch block for mock execution.
2471        // Use a clone so we don't mutate new_program yet
2472        let mut truncated_program = new_program.clone();
2473        self.only_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
2474
2475        // Execute - if this fails, we haven't modified self yet, so state is safe
2476        let outcome = ctx
2477            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
2478            .await
2479            .map_err(|err| {
2480                // TODO: sketch-api: Yeah, this needs to change. We need to
2481                // return the full error.
2482                Error {
2483                    msg: err.error.message().to_owned(),
2484                }
2485            })?;
2486
2487        #[cfg(not(feature = "artifact-graph"))]
2488        let new_object_ids = Vec::new();
2489        #[cfg(feature = "artifact-graph")]
2490        let new_object_ids = {
2491            // Extract the constraint ID from the execution outcome using source_range_to_object
2492            let constraint_id = outcome
2493                .source_range_to_object
2494                .get(&constraint_source_range)
2495                .copied()
2496                .ok_or_else(|| Error {
2497                    msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
2498                })?;
2499            vec![constraint_id]
2500        };
2501
2502        // Only now, after all operations succeeded, update self.program
2503        // This ensures state is only modified if everything succeeds
2504        self.program = new_program;
2505
2506        // Uses MockConfig::default() which has freedom_analysis: true
2507        let outcome = self.update_state_after_exec(outcome, true);
2508
2509        let src_delta = SourceDelta { text: new_source };
2510        let scene_graph_delta = SceneGraphDelta {
2511            new_graph: self.scene_graph.clone(),
2512            invalidates_ids: false,
2513            new_objects: new_object_ids,
2514            exec_outcome: outcome,
2515        };
2516        Ok((src_delta, scene_graph_delta))
2517    }
2518
2519    // Find constraints that reference the given segments to be deleted, and add
2520    // those to the constraint set to be deleted for cascading delete.
2521    fn add_dependent_constraints_to_delete(
2522        &self,
2523        sketch_id: ObjectId,
2524        segment_ids_set: &AhashIndexSet<ObjectId>,
2525        constraint_ids_set: &mut AhashIndexSet<ObjectId>,
2526    ) -> api::Result<()> {
2527        // Look up the sketch.
2528        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2529            msg: format!("Sketch not found: {sketch_id:?}"),
2530        })?;
2531        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2532            return Err(Error {
2533                msg: format!("Object is not a sketch: {sketch_object:?}"),
2534            });
2535        };
2536        for constraint_id in &sketch.constraints {
2537            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
2538                msg: format!("Constraint not found: {constraint_id:?}"),
2539            })?;
2540            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
2541                return Err(Error {
2542                    msg: format!("Object is not a constraint: {constraint_object:?}"),
2543                });
2544            };
2545            let depends_on_segment = match constraint {
2546                Constraint::Coincident(c) => c.segments.iter().any(|seg_id| {
2547                    // Check if the segment itself is being deleted
2548                    if segment_ids_set.contains(seg_id) {
2549                        return true;
2550                    }
2551                    // For points, also check if the owner line/arc is being deleted
2552                    let seg_object = self.scene_graph.objects.get(seg_id.0);
2553                    if let Some(obj) = seg_object
2554                        && let ObjectKind::Segment { segment } = &obj.kind
2555                        && let Segment::Point(pt) = segment
2556                        && let Some(owner_line_id) = pt.owner
2557                    {
2558                        return segment_ids_set.contains(&owner_line_id);
2559                    }
2560                    false
2561                }),
2562                Constraint::Distance(d) => d.points.iter().any(|pt_id| {
2563                    if segment_ids_set.contains(pt_id) {
2564                        return true;
2565                    }
2566                    let pt_object = self.scene_graph.objects.get(pt_id.0);
2567                    if let Some(obj) = pt_object
2568                        && let ObjectKind::Segment { segment } = &obj.kind
2569                        && let Segment::Point(pt) = segment
2570                        && let Some(owner_line_id) = pt.owner
2571                    {
2572                        return segment_ids_set.contains(&owner_line_id);
2573                    }
2574                    false
2575                }),
2576                Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
2577                Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
2578                Constraint::HorizontalDistance(d) => d.points.iter().any(|pt_id| {
2579                    let pt_object = self.scene_graph.objects.get(pt_id.0);
2580                    if let Some(obj) = pt_object
2581                        && let ObjectKind::Segment { segment } = &obj.kind
2582                        && let Segment::Point(pt) = segment
2583                        && let Some(owner_line_id) = pt.owner
2584                    {
2585                        return segment_ids_set.contains(&owner_line_id);
2586                    }
2587                    false
2588                }),
2589                Constraint::VerticalDistance(d) => d.points.iter().any(|pt_id| {
2590                    let pt_object = self.scene_graph.objects.get(pt_id.0);
2591                    if let Some(obj) = pt_object
2592                        && let ObjectKind::Segment { segment } = &obj.kind
2593                        && let Segment::Point(pt) = segment
2594                        && let Some(owner_line_id) = pt.owner
2595                    {
2596                        return segment_ids_set.contains(&owner_line_id);
2597                    }
2598                    false
2599                }),
2600                Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
2601                Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
2602                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
2603                    .lines
2604                    .iter()
2605                    .any(|line_id| segment_ids_set.contains(line_id)),
2606                Constraint::Parallel(parallel) => {
2607                    parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
2608                }
2609                Constraint::Perpendicular(perpendicular) => perpendicular
2610                    .lines
2611                    .iter()
2612                    .any(|line_id| segment_ids_set.contains(line_id)),
2613            };
2614            if depends_on_segment {
2615                constraint_ids_set.insert(*constraint_id);
2616            }
2617        }
2618        Ok(())
2619    }
2620
2621    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
2622        #[cfg(not(feature = "artifact-graph"))]
2623        {
2624            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
2625            outcome
2626        }
2627        #[cfg(feature = "artifact-graph")]
2628        {
2629            let mut outcome = outcome;
2630            let new_objects = std::mem::take(&mut outcome.scene_objects);
2631
2632            if freedom_analysis_ran {
2633                // When freedom analysis ran, replace the cache entirely with new values
2634                // Don't merge with old values since IDs might have changed
2635                self.point_freedom_cache.clear();
2636                for new_obj in &new_objects {
2637                    if let ObjectKind::Segment {
2638                        segment: crate::front::Segment::Point(point),
2639                    } = &new_obj.kind
2640                    {
2641                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
2642                    }
2643                }
2644                // Objects are already correct from the analysis, just use them as-is
2645                self.scene_graph.objects = new_objects;
2646            } else {
2647                // When freedom analysis didn't run, preserve old values and merge
2648                // Before replacing objects, extract and store freedom values from old objects
2649                for old_obj in &self.scene_graph.objects {
2650                    if let ObjectKind::Segment {
2651                        segment: crate::front::Segment::Point(point),
2652                    } = &old_obj.kind
2653                    {
2654                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
2655                    }
2656                }
2657
2658                // Update objects, preserving stored freedom values when new is Free (might be default)
2659                let mut updated_objects = Vec::with_capacity(new_objects.len());
2660                for new_obj in new_objects {
2661                    let mut obj = new_obj;
2662                    if let ObjectKind::Segment {
2663                        segment: crate::front::Segment::Point(point),
2664                    } = &mut obj.kind
2665                    {
2666                        let new_freedom = point.freedom;
2667                        // When freedom_analysis=false, new values are defaults (Free).
2668                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
2669                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
2670                        // Never preserve Conflict from cache - conflicts are transient and should only be set
2671                        // when there are actually unsatisfied constraints.
2672                        match new_freedom {
2673                            Freedom::Free => {
2674                                match self.point_freedom_cache.get(&obj.id).copied() {
2675                                    Some(Freedom::Conflict) => {
2676                                        // Don't preserve Conflict - conflicts are transient
2677                                        // Keep it as Free
2678                                    }
2679                                    Some(Freedom::Fixed) => {
2680                                        // Preserve Fixed cached value
2681                                        point.freedom = Freedom::Fixed;
2682                                    }
2683                                    Some(Freedom::Free) => {
2684                                        // If stored is also Free, keep Free (no change needed)
2685                                    }
2686                                    None => {
2687                                        // If no cached value, keep Free (default)
2688                                    }
2689                                }
2690                            }
2691                            Freedom::Fixed => {
2692                                // Use new value (already set)
2693                            }
2694                            Freedom::Conflict => {
2695                                // Use new value (already set)
2696                            }
2697                        }
2698                        // Store the new freedom value (even if it's Free, so we know it was set)
2699                        self.point_freedom_cache.insert(obj.id, point.freedom);
2700                    }
2701                    updated_objects.push(obj);
2702                }
2703
2704                self.scene_graph.objects = updated_objects;
2705            }
2706            outcome
2707        }
2708    }
2709
2710    fn only_sketch_block(
2711        &self,
2712        sketch_id: ObjectId,
2713        edit_kind: ChangeKind,
2714        ast: &mut ast::Node<ast::Program>,
2715    ) -> api::Result<()> {
2716        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2717            msg: format!("Sketch not found: {sketch_id:?}"),
2718        })?;
2719        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2720            return Err(Error {
2721                msg: format!("Object is not a sketch: {sketch_object:?}"),
2722            });
2723        };
2724        let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
2725        only_sketch_block(ast, sketch_block_range, edit_kind)
2726    }
2727
2728    fn mutate_ast(
2729        &mut self,
2730        ast: &mut ast::Node<ast::Program>,
2731        object_id: ObjectId,
2732        command: AstMutateCommand,
2733    ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
2734        let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
2735            msg: format!("Object not found: {object_id:?}"),
2736        })?;
2737        match &sketch_object.source {
2738            SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
2739            SourceRef::BackTrace { .. } => Err(Error {
2740                msg: "BackTrace source refs not supported yet".to_owned(),
2741            }),
2742        }
2743    }
2744}
2745
2746fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
2747    match source_ref {
2748        SourceRef::Simple { range } => Ok(*range),
2749        SourceRef::BackTrace { ranges } => {
2750            if ranges.len() != 1 {
2751                return Err(Error {
2752                    msg: format!(
2753                        "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
2754                        ranges.len(),
2755                    ),
2756                });
2757            }
2758            Ok(ranges[0])
2759        }
2760    }
2761}
2762
2763fn only_sketch_block(
2764    ast: &mut ast::Node<ast::Program>,
2765    sketch_block_range: SourceRange,
2766    edit_kind: ChangeKind,
2767) -> api::Result<()> {
2768    let r1 = sketch_block_range;
2769    let matches_range = |r2: SourceRange| -> bool {
2770        // We may have added items to the sketch block, so the end may not be an
2771        // exact match.
2772        match edit_kind {
2773            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
2774            // For edit, we don't know whether it grew or shrank.
2775            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
2776            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
2777            // No edit should be an exact match.
2778            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
2779        }
2780    };
2781    let mut found = false;
2782    for item in ast.body.iter_mut() {
2783        match item {
2784            ast::BodyItem::ImportStatement(_) => {}
2785            ast::BodyItem::ExpressionStatement(node) => {
2786                if matches_range(SourceRange::from(&*node))
2787                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
2788                {
2789                    sketch_block.is_being_edited = true;
2790                    found = true;
2791                    break;
2792                }
2793            }
2794            ast::BodyItem::VariableDeclaration(node) => {
2795                if matches_range(SourceRange::from(&node.declaration.init))
2796                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
2797                {
2798                    sketch_block.is_being_edited = true;
2799                    found = true;
2800                    break;
2801                }
2802            }
2803            ast::BodyItem::TypeDeclaration(_) => {}
2804            ast::BodyItem::ReturnStatement(node) => {
2805                if matches_range(SourceRange::from(&node.argument))
2806                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
2807                {
2808                    sketch_block.is_being_edited = true;
2809                    found = true;
2810                    break;
2811                }
2812            }
2813        }
2814    }
2815    if !found {
2816        return Err(Error {
2817            msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
2818        });
2819    }
2820
2821    Ok(())
2822}
2823
2824/// Return the AST expression referencing the variable at the given source ref.
2825/// If no such variable exists, insert a new variable declaration with the given
2826/// prefix.
2827///
2828/// This may return a complex expression referencing properties of the variable
2829/// (e.g., `line1.start`).
2830fn get_or_insert_ast_reference(
2831    ast: &mut ast::Node<ast::Program>,
2832    source_ref: &SourceRef,
2833    prefix: &str,
2834    property: Option<&str>,
2835) -> api::Result<ast::Expr> {
2836    let range = expect_single_source_range(source_ref)?;
2837    let command = AstMutateCommand::AddVariableDeclaration {
2838        prefix: prefix.to_owned(),
2839    };
2840    let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
2841    let AstMutateCommandReturn::Name(var_name) = ret else {
2842        return Err(Error {
2843            msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
2844        });
2845    };
2846    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
2847    let Some(property) = property else {
2848        // No property; just return the variable name.
2849        return Ok(var_expr);
2850    };
2851
2852    Ok(create_member_expression(var_expr, property))
2853}
2854
2855fn mutate_ast_node_by_source_range(
2856    ast: &mut ast::Node<ast::Program>,
2857    source_range: SourceRange,
2858    command: AstMutateCommand,
2859) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
2860    let mut context = AstMutateContext {
2861        source_range,
2862        command,
2863        defined_names_stack: Default::default(),
2864    };
2865    let control = dfs_mut(ast, &mut context);
2866    match control {
2867        ControlFlow::Continue(_) => Err(Error {
2868            msg: format!("Source range not found: {source_range:?}"),
2869        }),
2870        ControlFlow::Break(break_value) => break_value,
2871    }
2872}
2873
2874#[derive(Debug)]
2875struct AstMutateContext {
2876    source_range: SourceRange,
2877    command: AstMutateCommand,
2878    defined_names_stack: Vec<HashSet<String>>,
2879}
2880
2881#[derive(Debug)]
2882#[allow(clippy::large_enum_variant)]
2883enum AstMutateCommand {
2884    /// Add an expression statement to the sketch block.
2885    AddSketchBlockExprStmt {
2886        expr: ast::Expr,
2887    },
2888    AddVariableDeclaration {
2889        prefix: String,
2890    },
2891    EditPoint {
2892        at: ast::Expr,
2893    },
2894    EditLine {
2895        start: ast::Expr,
2896        end: ast::Expr,
2897        construction: Option<bool>,
2898    },
2899    EditArc {
2900        start: ast::Expr,
2901        end: ast::Expr,
2902        center: ast::Expr,
2903        construction: Option<bool>,
2904    },
2905    #[cfg(feature = "artifact-graph")]
2906    EditVarInitialValue {
2907        value: Number,
2908    },
2909    DeleteNode,
2910}
2911
2912#[derive(Debug)]
2913enum AstMutateCommandReturn {
2914    None,
2915    Name(String),
2916}
2917
2918impl Visitor for AstMutateContext {
2919    type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
2920    type Continue = ();
2921
2922    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
2923        filter_and_process(self, node)
2924    }
2925
2926    fn finish(&mut self, node: NodeMut<'_>) {
2927        match &node {
2928            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
2929                self.defined_names_stack.pop();
2930            }
2931            _ => {}
2932        }
2933    }
2934}
2935
2936fn filter_and_process(
2937    ctx: &mut AstMutateContext,
2938    node: NodeMut,
2939) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
2940    let Ok(node_range) = SourceRange::try_from(&node) else {
2941        // Nodes that can't be converted to a range aren't interesting.
2942        return TraversalReturn::new_continue(());
2943    };
2944    // If we're adding a variable declaration, we need to look at variable
2945    // declaration expressions to see if it already has a variable, before
2946    // continuing. The variable declaration's source range won't match the
2947    // target; its init expression will.
2948    if let NodeMut::VariableDeclaration(var_decl) = &node {
2949        let expr_range = SourceRange::from(&var_decl.declaration.init);
2950        if expr_range == ctx.source_range {
2951            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
2952                // We found the variable declaration expression. It doesn't need
2953                // to be added.
2954                return TraversalReturn::new_break(Ok((
2955                    node_range,
2956                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
2957                )));
2958            }
2959            if let AstMutateCommand::DeleteNode = &ctx.command {
2960                // We found the variable declaration. Delete the variable along
2961                // with the segment.
2962                return TraversalReturn {
2963                    mutate_body_item: MutateBodyItem::Delete,
2964                    control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
2965                };
2966            }
2967        }
2968    }
2969
2970    if let NodeMut::Program(program) = &node {
2971        ctx.defined_names_stack.push(find_defined_names(*program));
2972    } else if let NodeMut::SketchBlock(block) = &node {
2973        ctx.defined_names_stack.push(find_defined_names(&block.body));
2974    }
2975
2976    // Make sure the node matches the source range.
2977    if node_range != ctx.source_range {
2978        return TraversalReturn::new_continue(());
2979    }
2980    process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
2981}
2982
2983fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2984    match &ctx.command {
2985        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2986            if let NodeMut::SketchBlock(sketch_block) = node {
2987                sketch_block
2988                    .body
2989                    .items
2990                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
2991                        inner: ast::ExpressionStatement {
2992                            expression: expr.clone(),
2993                            digest: None,
2994                        },
2995                        start: Default::default(),
2996                        end: Default::default(),
2997                        module_id: Default::default(),
2998                        outer_attrs: Default::default(),
2999                        pre_comments: Default::default(),
3000                        comment_start: Default::default(),
3001                    }));
3002                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3003            }
3004        }
3005        AstMutateCommand::AddVariableDeclaration { prefix } => {
3006            if let NodeMut::VariableDeclaration(inner) = node {
3007                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
3008            }
3009            if let NodeMut::ExpressionStatement(expr_stmt) = node {
3010                let empty_defined_names = HashSet::new();
3011                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
3012                let Ok(name) = next_free_name(prefix, defined_names) else {
3013                    // TODO: Return an error instead?
3014                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3015                };
3016                let mutate_node =
3017                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
3018                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
3019                        ast::ItemVisibility::Default,
3020                        ast::VariableKind::Const,
3021                    ))));
3022                return TraversalReturn {
3023                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
3024                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
3025                };
3026            }
3027        }
3028        AstMutateCommand::EditPoint { at } => {
3029            if let NodeMut::CallExpressionKw(call) = node {
3030                if call.callee.name.name != POINT_FN {
3031                    return TraversalReturn::new_continue(());
3032                }
3033                // Update the arguments.
3034                for labeled_arg in &mut call.arguments {
3035                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
3036                        labeled_arg.arg = at.clone();
3037                    }
3038                }
3039                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3040            }
3041        }
3042        AstMutateCommand::EditLine {
3043            start,
3044            end,
3045            construction,
3046        } => {
3047            if let NodeMut::CallExpressionKw(call) = node {
3048                if call.callee.name.name != LINE_FN {
3049                    return TraversalReturn::new_continue(());
3050                }
3051                // Update the arguments.
3052                for labeled_arg in &mut call.arguments {
3053                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
3054                        labeled_arg.arg = start.clone();
3055                    }
3056                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
3057                        labeled_arg.arg = end.clone();
3058                    }
3059                }
3060                // Handle construction kwarg
3061                if let Some(construction_value) = construction {
3062                    let construction_exists = call
3063                        .arguments
3064                        .iter()
3065                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3066                    if *construction_value {
3067                        // Add or update construction=true
3068                        if construction_exists {
3069                            // Update existing construction kwarg
3070                            for labeled_arg in &mut call.arguments {
3071                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3072                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3073                                        value: ast::LiteralValue::Bool(true),
3074                                        raw: "true".to_string(),
3075                                        digest: None,
3076                                    })));
3077                                }
3078                            }
3079                        } else {
3080                            // Add new construction kwarg
3081                            call.arguments.push(ast::LabeledArg {
3082                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3083                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3084                                    value: ast::LiteralValue::Bool(true),
3085                                    raw: "true".to_string(),
3086                                    digest: None,
3087                                }))),
3088                            });
3089                        }
3090                    } else {
3091                        // Remove construction kwarg if it exists
3092                        call.arguments
3093                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3094                    }
3095                }
3096                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3097            }
3098        }
3099        AstMutateCommand::EditArc {
3100            start,
3101            end,
3102            center,
3103            construction,
3104        } => {
3105            if let NodeMut::CallExpressionKw(call) = node {
3106                if call.callee.name.name != ARC_FN {
3107                    return TraversalReturn::new_continue(());
3108                }
3109                // Update the arguments.
3110                for labeled_arg in &mut call.arguments {
3111                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
3112                        labeled_arg.arg = start.clone();
3113                    }
3114                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
3115                        labeled_arg.arg = end.clone();
3116                    }
3117                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
3118                        labeled_arg.arg = center.clone();
3119                    }
3120                }
3121                // Handle construction kwarg
3122                if let Some(construction_value) = construction {
3123                    let construction_exists = call
3124                        .arguments
3125                        .iter()
3126                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3127                    if *construction_value {
3128                        // Add or update construction=true
3129                        if construction_exists {
3130                            // Update existing construction kwarg
3131                            for labeled_arg in &mut call.arguments {
3132                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3133                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3134                                        value: ast::LiteralValue::Bool(true),
3135                                        raw: "true".to_string(),
3136                                        digest: None,
3137                                    })));
3138                                }
3139                            }
3140                        } else {
3141                            // Add new construction kwarg
3142                            call.arguments.push(ast::LabeledArg {
3143                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3144                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3145                                    value: ast::LiteralValue::Bool(true),
3146                                    raw: "true".to_string(),
3147                                    digest: None,
3148                                }))),
3149                            });
3150                        }
3151                    } else {
3152                        // Remove construction kwarg if it exists
3153                        call.arguments
3154                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3155                    }
3156                }
3157                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3158            }
3159        }
3160        #[cfg(feature = "artifact-graph")]
3161        AstMutateCommand::EditVarInitialValue { value } => {
3162            if let NodeMut::NumericLiteral(numeric_literal) = node {
3163                // Update the initial value.
3164                let Ok(literal) = to_source_number(*value) else {
3165                    return TraversalReturn::new_break(Err(Error {
3166                        msg: format!("Could not convert number to AST literal: {:?}", *value),
3167                    }));
3168                };
3169                *numeric_literal = ast::Node::no_src(literal);
3170                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3171            }
3172        }
3173        AstMutateCommand::DeleteNode => {
3174            return TraversalReturn {
3175                mutate_body_item: MutateBodyItem::Delete,
3176                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
3177            };
3178        }
3179    }
3180    TraversalReturn::new_continue(())
3181}
3182
3183struct FindSketchBlockSourceRange {
3184    /// The source range of the sketch block before mutation.
3185    target_before_mutation: SourceRange,
3186    /// The source range of the sketch block's last body item after mutation. We
3187    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
3188    /// shared reference.
3189    found: Cell<Option<SourceRange>>,
3190}
3191
3192impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
3193    type Error = crate::front::Error;
3194
3195    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
3196        let Ok(node_range) = SourceRange::try_from(&node) else {
3197            return Ok(true);
3198        };
3199
3200        if let crate::walk::Node::SketchBlock(sketch_block) = node {
3201            if node_range.module_id() == self.target_before_mutation.module_id()
3202                && node_range.start() == self.target_before_mutation.start()
3203                // End shouldn't match since we added something.
3204                && node_range.end() >= self.target_before_mutation.end()
3205            {
3206                self.found.set(sketch_block.body.items.last().map(SourceRange::from));
3207                return Ok(false);
3208            } else {
3209                // We found a different sketch block. No need to descend into
3210                // its children since sketch blocks cannot be nested.
3211                return Ok(true);
3212            }
3213        }
3214
3215        for child in node.children().iter() {
3216            if !child.visit(*self)? {
3217                return Ok(false);
3218            }
3219        }
3220
3221        Ok(true)
3222    }
3223}
3224
3225/// After adding an item to a sketch block, find the sketch block, and get the
3226/// source range of the added item. We assume that the added item is the last
3227/// item in the sketch block and that the sketch block's source range has grown,
3228/// but not moved from its starting offset.
3229///
3230/// TODO: Do we need to format *before* mutation in case formatting moves the
3231/// sketch block forward?
3232fn find_sketch_block_added_item(
3233    ast: &ast::Node<ast::Program>,
3234    range_before_mutation: SourceRange,
3235) -> api::Result<SourceRange> {
3236    let find = FindSketchBlockSourceRange {
3237        target_before_mutation: range_before_mutation,
3238        found: Cell::new(None),
3239    };
3240    let node = crate::walk::Node::from(ast);
3241    node.visit(&find)?;
3242    find.found.into_inner().ok_or_else(|| api::Error {
3243        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?"),
3244    })
3245}
3246
3247fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
3248    // TODO: Don't duplicate this from lib.rs Program.
3249    ast.recast_top(&Default::default(), 0)
3250}
3251
3252pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
3253    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
3254        inner: ast::ArrayExpression {
3255            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
3256            non_code_meta: Default::default(),
3257            digest: None,
3258        },
3259        start: Default::default(),
3260        end: Default::default(),
3261        module_id: Default::default(),
3262        outer_attrs: Default::default(),
3263        pre_comments: Default::default(),
3264        comment_start: Default::default(),
3265    })))
3266}
3267
3268fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
3269    match expr {
3270        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
3271            inner: ast::Literal::from(to_source_number(*number)?),
3272            start: Default::default(),
3273            end: Default::default(),
3274            module_id: Default::default(),
3275            outer_attrs: Default::default(),
3276            pre_comments: Default::default(),
3277            comment_start: Default::default(),
3278        }))),
3279        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
3280            inner: ast::SketchVar {
3281                initial: Some(Box::new(ast::Node {
3282                    inner: to_source_number(*number)?,
3283                    start: Default::default(),
3284                    end: Default::default(),
3285                    module_id: Default::default(),
3286                    outer_attrs: Default::default(),
3287                    pre_comments: Default::default(),
3288                    comment_start: Default::default(),
3289                })),
3290                digest: None,
3291            },
3292            start: Default::default(),
3293            end: Default::default(),
3294            module_id: Default::default(),
3295            outer_attrs: Default::default(),
3296            pre_comments: Default::default(),
3297            comment_start: Default::default(),
3298        }))),
3299        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
3300    }
3301}
3302
3303fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
3304    Ok(ast::NumericLiteral {
3305        value: number.value,
3306        suffix: number.units,
3307        raw: format_number_literal(number.value, number.units)?,
3308        digest: None,
3309    })
3310}
3311
3312pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
3313    ast::Expr::Name(Box::new(ast_name(name)))
3314}
3315
3316fn ast_name(name: String) -> ast::Node<ast::Name> {
3317    ast::Node {
3318        inner: ast::Name {
3319            name: ast::Node {
3320                inner: ast::Identifier { name, digest: None },
3321                start: Default::default(),
3322                end: Default::default(),
3323                module_id: Default::default(),
3324                outer_attrs: Default::default(),
3325                pre_comments: Default::default(),
3326                comment_start: Default::default(),
3327            },
3328            path: Vec::new(),
3329            abs_path: false,
3330            digest: None,
3331        },
3332        start: Default::default(),
3333        end: Default::default(),
3334        module_id: Default::default(),
3335        outer_attrs: Default::default(),
3336        pre_comments: Default::default(),
3337        comment_start: Default::default(),
3338    }
3339}
3340
3341pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
3342    ast::Name {
3343        name: ast::Node {
3344            inner: ast::Identifier {
3345                name: name.to_owned(),
3346                digest: None,
3347            },
3348            start: Default::default(),
3349            end: Default::default(),
3350            module_id: Default::default(),
3351            outer_attrs: Default::default(),
3352            pre_comments: Default::default(),
3353            comment_start: Default::default(),
3354        },
3355        path: vec![ast::Node::no_src(ast::Identifier {
3356            name: "sketch2".to_owned(),
3357            digest: None,
3358        })],
3359        abs_path: false,
3360        digest: None,
3361    }
3362}
3363
3364// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
3365
3366/// Create an AST node for sketch2::coincident([expr1, expr2])
3367pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
3368    // Create array [expr1, expr2]
3369    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3370        elements: vec![expr1, expr2],
3371        digest: None,
3372        non_code_meta: Default::default(),
3373    })));
3374
3375    // Create sketch2::coincident([...])
3376    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3377        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
3378        unlabeled: Some(array_expr),
3379        arguments: Default::default(),
3380        digest: None,
3381        non_code_meta: Default::default(),
3382    })))
3383}
3384
3385/// Create an AST node for sketch2::line(start = [...], end = [...])
3386pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
3387    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3388        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
3389        unlabeled: None,
3390        arguments: vec![
3391            ast::LabeledArg {
3392                label: Some(ast::Identifier::new(LINE_START_PARAM)),
3393                arg: start_ast,
3394            },
3395            ast::LabeledArg {
3396                label: Some(ast::Identifier::new(LINE_END_PARAM)),
3397                arg: end_ast,
3398            },
3399        ],
3400        digest: None,
3401        non_code_meta: Default::default(),
3402    })))
3403}
3404
3405/// Create an AST node for sketch2::horizontal(line)
3406pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
3407    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3408        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
3409        unlabeled: Some(line_expr),
3410        arguments: Default::default(),
3411        digest: None,
3412        non_code_meta: Default::default(),
3413    })))
3414}
3415
3416/// Create an AST node for sketch2::vertical(line)
3417pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
3418    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3419        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
3420        unlabeled: Some(line_expr),
3421        arguments: Default::default(),
3422        digest: None,
3423        non_code_meta: Default::default(),
3424    })))
3425}
3426
3427/// Create a member expression like object.property (e.g., line1.end)
3428pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
3429    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
3430        object: object_expr,
3431        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
3432            name: ast::Node::no_src(ast::Identifier {
3433                name: property.to_string(),
3434                digest: None,
3435            }),
3436            path: Vec::new(),
3437            abs_path: false,
3438            digest: None,
3439        }))),
3440        computed: false,
3441        digest: None,
3442    })))
3443}
3444
3445/// Create an AST node for sketch2::equalLength([line1, line2])
3446pub(crate) fn create_equal_length_ast(line1_expr: ast::Expr, line2_expr: ast::Expr) -> ast::Expr {
3447    // Create array [line1, line2]
3448    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3449        elements: vec![line1_expr, line2_expr],
3450        digest: None,
3451        non_code_meta: Default::default(),
3452    })));
3453
3454    // Create sketch2::equalLength([...])
3455    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3456        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
3457        unlabeled: Some(array_expr),
3458        arguments: Default::default(),
3459        digest: None,
3460        non_code_meta: Default::default(),
3461    })))
3462}
3463
3464#[cfg(test)]
3465mod tests {
3466    use super::*;
3467    use crate::{
3468        engine::PlaneName,
3469        front::{Distance, Object, Plane, Sketch},
3470        frontend::sketch::Vertical,
3471        pretty::NumericSuffix,
3472    };
3473
3474    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
3475        for object in &scene_graph.objects {
3476            if let ObjectKind::Sketch(_) = &object.kind {
3477                return Some(object);
3478            }
3479        }
3480        None
3481    }
3482
3483    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
3484        for object in &scene_graph.objects {
3485            if let ObjectKind::Face(_) = &object.kind {
3486                return Some(object);
3487            }
3488        }
3489        None
3490    }
3491
3492    #[track_caller]
3493    fn expect_sketch(object: &Object) -> &Sketch {
3494        if let ObjectKind::Sketch(sketch) = &object.kind {
3495            sketch
3496        } else {
3497            panic!("Object is not a sketch: {:?}", object);
3498        }
3499    }
3500
3501    #[tokio::test(flavor = "multi_thread")]
3502    async fn test_new_sketch_add_point_edit_point() {
3503        let program = Program::empty();
3504
3505        let mut frontend = FrontendState::new();
3506        frontend.program = program;
3507
3508        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3509        let mock_ctx = ExecutorContext::new_mock(None).await;
3510        let version = Version(0);
3511
3512        let sketch_args = SketchCtor {
3513            on: PlaneName::Xy.to_string(),
3514        };
3515        let (_src_delta, scene_delta, sketch_id) = frontend
3516            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3517            .await
3518            .unwrap();
3519        assert_eq!(sketch_id, ObjectId(1));
3520        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3521        let sketch_object = &scene_delta.new_graph.objects[1];
3522        assert_eq!(sketch_object.id, ObjectId(1));
3523        assert_eq!(
3524            sketch_object.kind,
3525            ObjectKind::Sketch(Sketch {
3526                args: SketchCtor {
3527                    on: PlaneName::Xy.to_string()
3528                },
3529                plane: ObjectId(0),
3530                segments: vec![],
3531                constraints: vec![],
3532            })
3533        );
3534        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3535
3536        let point_ctor = PointCtor {
3537            position: Point2d {
3538                x: Expr::Number(Number {
3539                    value: 1.0,
3540                    units: NumericSuffix::Inch,
3541                }),
3542                y: Expr::Number(Number {
3543                    value: 2.0,
3544                    units: NumericSuffix::Inch,
3545                }),
3546            },
3547        };
3548        let segment = SegmentCtor::Point(point_ctor);
3549        let (src_delta, scene_delta) = frontend
3550            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3551            .await
3552            .unwrap();
3553        assert_eq!(
3554            src_delta.text.as_str(),
3555            "@settings(experimentalFeatures = allow)
3556
3557sketch(on = XY) {
3558  sketch2::point(at = [1in, 2in])
3559}
3560"
3561        );
3562        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
3563        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3564        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3565            assert_eq!(scene_object.id.0, i);
3566        }
3567
3568        let point_id = *scene_delta.new_objects.last().unwrap();
3569
3570        let point_ctor = PointCtor {
3571            position: Point2d {
3572                x: Expr::Number(Number {
3573                    value: 3.0,
3574                    units: NumericSuffix::Inch,
3575                }),
3576                y: Expr::Number(Number {
3577                    value: 4.0,
3578                    units: NumericSuffix::Inch,
3579                }),
3580            },
3581        };
3582        let segments = vec![ExistingSegmentCtor {
3583            id: point_id,
3584            ctor: SegmentCtor::Point(point_ctor),
3585        }];
3586        let (src_delta, scene_delta) = frontend
3587            .edit_segments(&mock_ctx, version, sketch_id, segments)
3588            .await
3589            .unwrap();
3590        assert_eq!(
3591            src_delta.text.as_str(),
3592            "@settings(experimentalFeatures = allow)
3593
3594sketch(on = XY) {
3595  sketch2::point(at = [3in, 4in])
3596}
3597"
3598        );
3599        assert_eq!(scene_delta.new_objects, vec![]);
3600        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3601
3602        ctx.close().await;
3603        mock_ctx.close().await;
3604    }
3605
3606    #[tokio::test(flavor = "multi_thread")]
3607    async fn test_new_sketch_add_line_edit_line() {
3608        let program = Program::empty();
3609
3610        let mut frontend = FrontendState::new();
3611        frontend.program = program;
3612
3613        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3614        let mock_ctx = ExecutorContext::new_mock(None).await;
3615        let version = Version(0);
3616
3617        let sketch_args = SketchCtor {
3618            on: PlaneName::Xy.to_string(),
3619        };
3620        let (_src_delta, scene_delta, sketch_id) = frontend
3621            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3622            .await
3623            .unwrap();
3624        assert_eq!(sketch_id, ObjectId(1));
3625        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3626        let sketch_object = &scene_delta.new_graph.objects[1];
3627        assert_eq!(sketch_object.id, ObjectId(1));
3628        assert_eq!(
3629            sketch_object.kind,
3630            ObjectKind::Sketch(Sketch {
3631                args: SketchCtor {
3632                    on: PlaneName::Xy.to_string()
3633                },
3634                plane: ObjectId(0),
3635                segments: vec![],
3636                constraints: vec![],
3637            })
3638        );
3639        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3640
3641        let line_ctor = LineCtor {
3642            start: Point2d {
3643                x: Expr::Number(Number {
3644                    value: 0.0,
3645                    units: NumericSuffix::Mm,
3646                }),
3647                y: Expr::Number(Number {
3648                    value: 0.0,
3649                    units: NumericSuffix::Mm,
3650                }),
3651            },
3652            end: Point2d {
3653                x: Expr::Number(Number {
3654                    value: 10.0,
3655                    units: NumericSuffix::Mm,
3656                }),
3657                y: Expr::Number(Number {
3658                    value: 10.0,
3659                    units: NumericSuffix::Mm,
3660                }),
3661            },
3662            construction: None,
3663        };
3664        let segment = SegmentCtor::Line(line_ctor);
3665        let (src_delta, scene_delta) = frontend
3666            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3667            .await
3668            .unwrap();
3669        assert_eq!(
3670            src_delta.text.as_str(),
3671            "@settings(experimentalFeatures = allow)
3672
3673sketch(on = XY) {
3674  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3675}
3676"
3677        );
3678        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3679        assert_eq!(scene_delta.new_graph.objects.len(), 5);
3680        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3681            assert_eq!(scene_object.id.0, i);
3682        }
3683
3684        // The new objects are the end points and then the line.
3685        let line = *scene_delta.new_objects.last().unwrap();
3686
3687        let line_ctor = LineCtor {
3688            start: Point2d {
3689                x: Expr::Number(Number {
3690                    value: 1.0,
3691                    units: NumericSuffix::Mm,
3692                }),
3693                y: Expr::Number(Number {
3694                    value: 2.0,
3695                    units: NumericSuffix::Mm,
3696                }),
3697            },
3698            end: Point2d {
3699                x: Expr::Number(Number {
3700                    value: 13.0,
3701                    units: NumericSuffix::Mm,
3702                }),
3703                y: Expr::Number(Number {
3704                    value: 14.0,
3705                    units: NumericSuffix::Mm,
3706                }),
3707            },
3708            construction: None,
3709        };
3710        let segments = vec![ExistingSegmentCtor {
3711            id: line,
3712            ctor: SegmentCtor::Line(line_ctor),
3713        }];
3714        let (src_delta, scene_delta) = frontend
3715            .edit_segments(&mock_ctx, version, sketch_id, segments)
3716            .await
3717            .unwrap();
3718        assert_eq!(
3719            src_delta.text.as_str(),
3720            "@settings(experimentalFeatures = allow)
3721
3722sketch(on = XY) {
3723  sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
3724}
3725"
3726        );
3727        assert_eq!(scene_delta.new_objects, vec![]);
3728        assert_eq!(scene_delta.new_graph.objects.len(), 5);
3729
3730        ctx.close().await;
3731        mock_ctx.close().await;
3732    }
3733
3734    #[tokio::test(flavor = "multi_thread")]
3735    async fn test_new_sketch_add_arc_edit_arc() {
3736        let program = Program::empty();
3737
3738        let mut frontend = FrontendState::new();
3739        frontend.program = program;
3740
3741        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3742        let mock_ctx = ExecutorContext::new_mock(None).await;
3743        let version = Version(0);
3744
3745        let sketch_args = SketchCtor {
3746            on: PlaneName::Xy.to_string(),
3747        };
3748        let (_src_delta, scene_delta, sketch_id) = frontend
3749            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3750            .await
3751            .unwrap();
3752        assert_eq!(sketch_id, ObjectId(1));
3753        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3754        let sketch_object = &scene_delta.new_graph.objects[1];
3755        assert_eq!(sketch_object.id, ObjectId(1));
3756        assert_eq!(
3757            sketch_object.kind,
3758            ObjectKind::Sketch(Sketch {
3759                args: SketchCtor {
3760                    on: PlaneName::Xy.to_string(),
3761                },
3762                plane: ObjectId(0),
3763                segments: vec![],
3764                constraints: vec![],
3765            })
3766        );
3767        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3768
3769        let arc_ctor = ArcCtor {
3770            start: Point2d {
3771                x: Expr::Var(Number {
3772                    value: 0.0,
3773                    units: NumericSuffix::Mm,
3774                }),
3775                y: Expr::Var(Number {
3776                    value: 0.0,
3777                    units: NumericSuffix::Mm,
3778                }),
3779            },
3780            end: Point2d {
3781                x: Expr::Var(Number {
3782                    value: 10.0,
3783                    units: NumericSuffix::Mm,
3784                }),
3785                y: Expr::Var(Number {
3786                    value: 10.0,
3787                    units: NumericSuffix::Mm,
3788                }),
3789            },
3790            center: Point2d {
3791                x: Expr::Var(Number {
3792                    value: 10.0,
3793                    units: NumericSuffix::Mm,
3794                }),
3795                y: Expr::Var(Number {
3796                    value: 0.0,
3797                    units: NumericSuffix::Mm,
3798                }),
3799            },
3800            construction: None,
3801        };
3802        let segment = SegmentCtor::Arc(arc_ctor);
3803        let (src_delta, scene_delta) = frontend
3804            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3805            .await
3806            .unwrap();
3807        assert_eq!(
3808            src_delta.text.as_str(),
3809            "@settings(experimentalFeatures = allow)
3810
3811sketch(on = XY) {
3812  sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
3813}
3814"
3815        );
3816        assert_eq!(
3817            scene_delta.new_objects,
3818            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
3819        );
3820        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3821            assert_eq!(scene_object.id.0, i);
3822        }
3823        assert_eq!(scene_delta.new_graph.objects.len(), 6);
3824
3825        // The new objects are the end points, the center, and then the arc.
3826        let arc = *scene_delta.new_objects.last().unwrap();
3827
3828        let arc_ctor = ArcCtor {
3829            start: Point2d {
3830                x: Expr::Var(Number {
3831                    value: 1.0,
3832                    units: NumericSuffix::Mm,
3833                }),
3834                y: Expr::Var(Number {
3835                    value: 2.0,
3836                    units: NumericSuffix::Mm,
3837                }),
3838            },
3839            end: Point2d {
3840                x: Expr::Var(Number {
3841                    value: 13.0,
3842                    units: NumericSuffix::Mm,
3843                }),
3844                y: Expr::Var(Number {
3845                    value: 14.0,
3846                    units: NumericSuffix::Mm,
3847                }),
3848            },
3849            center: Point2d {
3850                x: Expr::Var(Number {
3851                    value: 13.0,
3852                    units: NumericSuffix::Mm,
3853                }),
3854                y: Expr::Var(Number {
3855                    value: 2.0,
3856                    units: NumericSuffix::Mm,
3857                }),
3858            },
3859            construction: None,
3860        };
3861        let segments = vec![ExistingSegmentCtor {
3862            id: arc,
3863            ctor: SegmentCtor::Arc(arc_ctor),
3864        }];
3865        let (src_delta, scene_delta) = frontend
3866            .edit_segments(&mock_ctx, version, sketch_id, segments)
3867            .await
3868            .unwrap();
3869        assert_eq!(
3870            src_delta.text.as_str(),
3871            "@settings(experimentalFeatures = allow)
3872
3873sketch(on = XY) {
3874  sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
3875}
3876"
3877        );
3878        assert_eq!(scene_delta.new_objects, vec![]);
3879        assert_eq!(scene_delta.new_graph.objects.len(), 6);
3880
3881        ctx.close().await;
3882        mock_ctx.close().await;
3883    }
3884
3885    #[tokio::test(flavor = "multi_thread")]
3886    async fn test_add_line_when_sketch_block_uses_variable() {
3887        let initial_source = "@settings(experimentalFeatures = allow)
3888
3889s = sketch(on = XY) {}
3890";
3891
3892        let program = Program::parse(initial_source).unwrap().0.unwrap();
3893
3894        let mut frontend = FrontendState::new();
3895
3896        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3897        let mock_ctx = ExecutorContext::new_mock(None).await;
3898        let version = Version(0);
3899
3900        frontend.hack_set_program(&ctx, program).await.unwrap();
3901        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3902        let sketch_id = sketch_object.id;
3903
3904        let line_ctor = LineCtor {
3905            start: Point2d {
3906                x: Expr::Number(Number {
3907                    value: 0.0,
3908                    units: NumericSuffix::Mm,
3909                }),
3910                y: Expr::Number(Number {
3911                    value: 0.0,
3912                    units: NumericSuffix::Mm,
3913                }),
3914            },
3915            end: Point2d {
3916                x: Expr::Number(Number {
3917                    value: 10.0,
3918                    units: NumericSuffix::Mm,
3919                }),
3920                y: Expr::Number(Number {
3921                    value: 10.0,
3922                    units: NumericSuffix::Mm,
3923                }),
3924            },
3925            construction: None,
3926        };
3927        let segment = SegmentCtor::Line(line_ctor);
3928        let (src_delta, scene_delta) = frontend
3929            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3930            .await
3931            .unwrap();
3932        assert_eq!(
3933            src_delta.text.as_str(),
3934            "@settings(experimentalFeatures = allow)
3935
3936s = sketch(on = XY) {
3937  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3938}
3939"
3940        );
3941        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3942        assert_eq!(scene_delta.new_graph.objects.len(), 5);
3943
3944        ctx.close().await;
3945        mock_ctx.close().await;
3946    }
3947
3948    #[tokio::test(flavor = "multi_thread")]
3949    async fn test_new_sketch_add_line_delete_sketch() {
3950        let program = Program::empty();
3951
3952        let mut frontend = FrontendState::new();
3953        frontend.program = program;
3954
3955        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3956        let mock_ctx = ExecutorContext::new_mock(None).await;
3957        let version = Version(0);
3958
3959        let sketch_args = SketchCtor {
3960            on: PlaneName::Xy.to_string(),
3961        };
3962        let (_src_delta, scene_delta, sketch_id) = frontend
3963            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3964            .await
3965            .unwrap();
3966        assert_eq!(sketch_id, ObjectId(1));
3967        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3968        let sketch_object = &scene_delta.new_graph.objects[1];
3969        assert_eq!(sketch_object.id, ObjectId(1));
3970        assert_eq!(
3971            sketch_object.kind,
3972            ObjectKind::Sketch(Sketch {
3973                args: SketchCtor {
3974                    on: PlaneName::Xy.to_string()
3975                },
3976                plane: ObjectId(0),
3977                segments: vec![],
3978                constraints: vec![],
3979            })
3980        );
3981        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3982
3983        let line_ctor = LineCtor {
3984            start: Point2d {
3985                x: Expr::Number(Number {
3986                    value: 0.0,
3987                    units: NumericSuffix::Mm,
3988                }),
3989                y: Expr::Number(Number {
3990                    value: 0.0,
3991                    units: NumericSuffix::Mm,
3992                }),
3993            },
3994            end: Point2d {
3995                x: Expr::Number(Number {
3996                    value: 10.0,
3997                    units: NumericSuffix::Mm,
3998                }),
3999                y: Expr::Number(Number {
4000                    value: 10.0,
4001                    units: NumericSuffix::Mm,
4002                }),
4003            },
4004            construction: None,
4005        };
4006        let segment = SegmentCtor::Line(line_ctor);
4007        let (src_delta, scene_delta) = frontend
4008            .add_segment(&mock_ctx, version, sketch_id, segment, None)
4009            .await
4010            .unwrap();
4011        assert_eq!(
4012            src_delta.text.as_str(),
4013            "@settings(experimentalFeatures = allow)
4014
4015sketch(on = XY) {
4016  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
4017}
4018"
4019        );
4020        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4021
4022        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4023        assert_eq!(
4024            src_delta.text.as_str(),
4025            "@settings(experimentalFeatures = allow)
4026"
4027        );
4028        assert_eq!(scene_delta.new_graph.objects.len(), 0);
4029
4030        ctx.close().await;
4031        mock_ctx.close().await;
4032    }
4033
4034    #[tokio::test(flavor = "multi_thread")]
4035    async fn test_delete_sketch_when_sketch_block_uses_variable() {
4036        let initial_source = "@settings(experimentalFeatures = allow)
4037
4038s = sketch(on = XY) {}
4039";
4040
4041        let program = Program::parse(initial_source).unwrap().0.unwrap();
4042
4043        let mut frontend = FrontendState::new();
4044
4045        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4046        let mock_ctx = ExecutorContext::new_mock(None).await;
4047        let version = Version(0);
4048
4049        frontend.hack_set_program(&ctx, program).await.unwrap();
4050        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4051        let sketch_id = sketch_object.id;
4052
4053        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4054        assert_eq!(
4055            src_delta.text.as_str(),
4056            "@settings(experimentalFeatures = allow)
4057"
4058        );
4059        assert_eq!(scene_delta.new_graph.objects.len(), 0);
4060
4061        ctx.close().await;
4062        mock_ctx.close().await;
4063    }
4064
4065    #[tokio::test(flavor = "multi_thread")]
4066    async fn test_edit_line_when_editing_its_start_point() {
4067        let initial_source = "\
4068@settings(experimentalFeatures = allow)
4069
4070sketch(on = XY) {
4071  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4072}
4073";
4074
4075        let program = Program::parse(initial_source).unwrap().0.unwrap();
4076
4077        let mut frontend = FrontendState::new();
4078
4079        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4080        let mock_ctx = ExecutorContext::new_mock(None).await;
4081        let version = Version(0);
4082
4083        frontend.hack_set_program(&ctx, program).await.unwrap();
4084        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4085        let sketch_id = sketch_object.id;
4086        let sketch = expect_sketch(sketch_object);
4087
4088        let point_id = *sketch.segments.first().unwrap();
4089
4090        let point_ctor = PointCtor {
4091            position: Point2d {
4092                x: Expr::Var(Number {
4093                    value: 5.0,
4094                    units: NumericSuffix::Inch,
4095                }),
4096                y: Expr::Var(Number {
4097                    value: 6.0,
4098                    units: NumericSuffix::Inch,
4099                }),
4100            },
4101        };
4102        let segments = vec![ExistingSegmentCtor {
4103            id: point_id,
4104            ctor: SegmentCtor::Point(point_ctor),
4105        }];
4106        let (src_delta, scene_delta) = frontend
4107            .edit_segments(&mock_ctx, version, sketch_id, segments)
4108            .await
4109            .unwrap();
4110        assert_eq!(
4111            src_delta.text.as_str(),
4112            "\
4113@settings(experimentalFeatures = allow)
4114
4115sketch(on = XY) {
4116  sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
4117}
4118"
4119        );
4120        assert_eq!(scene_delta.new_objects, vec![]);
4121        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4122
4123        ctx.close().await;
4124        mock_ctx.close().await;
4125    }
4126
4127    #[tokio::test(flavor = "multi_thread")]
4128    async fn test_edit_line_when_editing_its_end_point() {
4129        let initial_source = "\
4130@settings(experimentalFeatures = allow)
4131
4132sketch(on = XY) {
4133  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4134}
4135";
4136
4137        let program = Program::parse(initial_source).unwrap().0.unwrap();
4138
4139        let mut frontend = FrontendState::new();
4140
4141        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4142        let mock_ctx = ExecutorContext::new_mock(None).await;
4143        let version = Version(0);
4144
4145        frontend.hack_set_program(&ctx, program).await.unwrap();
4146        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4147        let sketch_id = sketch_object.id;
4148        let sketch = expect_sketch(sketch_object);
4149        let point_id = *sketch.segments.get(1).unwrap();
4150
4151        let point_ctor = PointCtor {
4152            position: Point2d {
4153                x: Expr::Var(Number {
4154                    value: 5.0,
4155                    units: NumericSuffix::Inch,
4156                }),
4157                y: Expr::Var(Number {
4158                    value: 6.0,
4159                    units: NumericSuffix::Inch,
4160                }),
4161            },
4162        };
4163        let segments = vec![ExistingSegmentCtor {
4164            id: point_id,
4165            ctor: SegmentCtor::Point(point_ctor),
4166        }];
4167        let (src_delta, scene_delta) = frontend
4168            .edit_segments(&mock_ctx, version, sketch_id, segments)
4169            .await
4170            .unwrap();
4171        assert_eq!(
4172            src_delta.text.as_str(),
4173            "\
4174@settings(experimentalFeatures = allow)
4175
4176sketch(on = XY) {
4177  sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
4178}
4179"
4180        );
4181        assert_eq!(scene_delta.new_objects, vec![]);
4182        assert_eq!(
4183            scene_delta.new_graph.objects.len(),
4184            5,
4185            "{:#?}",
4186            scene_delta.new_graph.objects
4187        );
4188
4189        ctx.close().await;
4190        mock_ctx.close().await;
4191    }
4192
4193    #[tokio::test(flavor = "multi_thread")]
4194    async fn test_edit_line_with_coincident_feedback() {
4195        let initial_source = "\
4196@settings(experimentalFeatures = allow)
4197
4198sketch(on = XY) {
4199  line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
4200  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4201  line1.start.at[0] == 0
4202  line1.start.at[1] == 0
4203  sketch2::coincident([line1.end, line2.start])
4204  sketch2::equalLength([line1, line2])
4205}
4206";
4207
4208        let program = Program::parse(initial_source).unwrap().0.unwrap();
4209
4210        let mut frontend = FrontendState::new();
4211
4212        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4213        let mock_ctx = ExecutorContext::new_mock(None).await;
4214        let version = Version(0);
4215
4216        frontend.hack_set_program(&ctx, program).await.unwrap();
4217        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4218        let sketch_id = sketch_object.id;
4219        let sketch = expect_sketch(sketch_object);
4220        let line2_end_id = *sketch.segments.get(4).unwrap();
4221
4222        let segments = vec![ExistingSegmentCtor {
4223            id: line2_end_id,
4224            ctor: SegmentCtor::Point(PointCtor {
4225                position: Point2d {
4226                    x: Expr::Var(Number {
4227                        value: 9.0,
4228                        units: NumericSuffix::None,
4229                    }),
4230                    y: Expr::Var(Number {
4231                        value: 10.0,
4232                        units: NumericSuffix::None,
4233                    }),
4234                },
4235            }),
4236        }];
4237        let (src_delta, scene_delta) = frontend
4238            .edit_segments(&mock_ctx, version, sketch_id, segments)
4239            .await
4240            .unwrap();
4241        assert_eq!(
4242            src_delta.text.as_str(),
4243            "\
4244@settings(experimentalFeatures = allow)
4245
4246sketch(on = XY) {
4247  line1 = sketch2::line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
4248  line2 = sketch2::line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
4249line1.start.at[0] == 0
4250line1.start.at[1] == 0
4251  sketch2::coincident([line1.end, line2.start])
4252  sketch2::equalLength([line1, line2])
4253}
4254"
4255        );
4256        assert_eq!(
4257            scene_delta.new_graph.objects.len(),
4258            10,
4259            "{:#?}",
4260            scene_delta.new_graph.objects
4261        );
4262
4263        ctx.close().await;
4264        mock_ctx.close().await;
4265    }
4266
4267    #[tokio::test(flavor = "multi_thread")]
4268    async fn test_delete_point_without_var() {
4269        let initial_source = "\
4270@settings(experimentalFeatures = allow)
4271
4272sketch(on = XY) {
4273  sketch2::point(at = [var 1, var 2])
4274  sketch2::point(at = [var 3, var 4])
4275  sketch2::point(at = [var 5, var 6])
4276}
4277";
4278
4279        let program = Program::parse(initial_source).unwrap().0.unwrap();
4280
4281        let mut frontend = FrontendState::new();
4282
4283        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4284        let mock_ctx = ExecutorContext::new_mock(None).await;
4285        let version = Version(0);
4286
4287        frontend.hack_set_program(&ctx, program).await.unwrap();
4288        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4289        let sketch_id = sketch_object.id;
4290        let sketch = expect_sketch(sketch_object);
4291
4292        let point_id = *sketch.segments.get(1).unwrap();
4293
4294        let (src_delta, scene_delta) = frontend
4295            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4296            .await
4297            .unwrap();
4298        assert_eq!(
4299            src_delta.text.as_str(),
4300            "\
4301@settings(experimentalFeatures = allow)
4302
4303sketch(on = XY) {
4304  sketch2::point(at = [var 1mm, var 2mm])
4305  sketch2::point(at = [var 5mm, var 6mm])
4306}
4307"
4308        );
4309        assert_eq!(scene_delta.new_objects, vec![]);
4310        assert_eq!(scene_delta.new_graph.objects.len(), 4);
4311
4312        ctx.close().await;
4313        mock_ctx.close().await;
4314    }
4315
4316    #[tokio::test(flavor = "multi_thread")]
4317    async fn test_delete_point_with_var() {
4318        let initial_source = "\
4319@settings(experimentalFeatures = allow)
4320
4321sketch(on = XY) {
4322  sketch2::point(at = [var 1, var 2])
4323  point1 = sketch2::point(at = [var 3, var 4])
4324  sketch2::point(at = [var 5, var 6])
4325}
4326";
4327
4328        let program = Program::parse(initial_source).unwrap().0.unwrap();
4329
4330        let mut frontend = FrontendState::new();
4331
4332        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4333        let mock_ctx = ExecutorContext::new_mock(None).await;
4334        let version = Version(0);
4335
4336        frontend.hack_set_program(&ctx, program).await.unwrap();
4337        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4338        let sketch_id = sketch_object.id;
4339        let sketch = expect_sketch(sketch_object);
4340
4341        let point_id = *sketch.segments.get(1).unwrap();
4342
4343        let (src_delta, scene_delta) = frontend
4344            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4345            .await
4346            .unwrap();
4347        assert_eq!(
4348            src_delta.text.as_str(),
4349            "\
4350@settings(experimentalFeatures = allow)
4351
4352sketch(on = XY) {
4353  sketch2::point(at = [var 1mm, var 2mm])
4354  sketch2::point(at = [var 5mm, var 6mm])
4355}
4356"
4357        );
4358        assert_eq!(scene_delta.new_objects, vec![]);
4359        assert_eq!(scene_delta.new_graph.objects.len(), 4);
4360
4361        ctx.close().await;
4362        mock_ctx.close().await;
4363    }
4364
4365    #[tokio::test(flavor = "multi_thread")]
4366    async fn test_delete_multiple_points() {
4367        let initial_source = "\
4368@settings(experimentalFeatures = allow)
4369
4370sketch(on = XY) {
4371  sketch2::point(at = [var 1, var 2])
4372  point1 = sketch2::point(at = [var 3, var 4])
4373  sketch2::point(at = [var 5, var 6])
4374}
4375";
4376
4377        let program = Program::parse(initial_source).unwrap().0.unwrap();
4378
4379        let mut frontend = FrontendState::new();
4380
4381        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4382        let mock_ctx = ExecutorContext::new_mock(None).await;
4383        let version = Version(0);
4384
4385        frontend.hack_set_program(&ctx, program).await.unwrap();
4386        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4387        let sketch_id = sketch_object.id;
4388
4389        let sketch = expect_sketch(sketch_object);
4390
4391        let point1_id = *sketch.segments.first().unwrap();
4392        let point2_id = *sketch.segments.get(1).unwrap();
4393
4394        let (src_delta, scene_delta) = frontend
4395            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
4396            .await
4397            .unwrap();
4398        assert_eq!(
4399            src_delta.text.as_str(),
4400            "\
4401@settings(experimentalFeatures = allow)
4402
4403sketch(on = XY) {
4404  sketch2::point(at = [var 5mm, var 6mm])
4405}
4406"
4407        );
4408        assert_eq!(scene_delta.new_objects, vec![]);
4409        assert_eq!(scene_delta.new_graph.objects.len(), 3);
4410
4411        ctx.close().await;
4412        mock_ctx.close().await;
4413    }
4414
4415    #[tokio::test(flavor = "multi_thread")]
4416    async fn test_delete_coincident_constraint() {
4417        let initial_source = "\
4418@settings(experimentalFeatures = allow)
4419
4420sketch(on = XY) {
4421  point1 = sketch2::point(at = [var 1, var 2])
4422  point2 = sketch2::point(at = [var 3, var 4])
4423  sketch2::coincident([point1, point2])
4424  sketch2::point(at = [var 5, var 6])
4425}
4426";
4427
4428        let program = Program::parse(initial_source).unwrap().0.unwrap();
4429
4430        let mut frontend = FrontendState::new();
4431
4432        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4433        let mock_ctx = ExecutorContext::new_mock(None).await;
4434        let version = Version(0);
4435
4436        frontend.hack_set_program(&ctx, program).await.unwrap();
4437        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4438        let sketch_id = sketch_object.id;
4439        let sketch = expect_sketch(sketch_object);
4440
4441        let coincident_id = *sketch.constraints.first().unwrap();
4442
4443        let (src_delta, scene_delta) = frontend
4444            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4445            .await
4446            .unwrap();
4447        assert_eq!(
4448            src_delta.text.as_str(),
4449            "\
4450@settings(experimentalFeatures = allow)
4451
4452sketch(on = XY) {
4453  point1 = sketch2::point(at = [var 1mm, var 2mm])
4454  point2 = sketch2::point(at = [var 3mm, var 4mm])
4455  sketch2::point(at = [var 5mm, var 6mm])
4456}
4457"
4458        );
4459        assert_eq!(scene_delta.new_objects, vec![]);
4460        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4461
4462        ctx.close().await;
4463        mock_ctx.close().await;
4464    }
4465
4466    #[tokio::test(flavor = "multi_thread")]
4467    async fn test_delete_line_cascades_to_coincident_constraint() {
4468        let initial_source = "\
4469@settings(experimentalFeatures = allow)
4470
4471sketch(on = XY) {
4472  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4473  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4474  sketch2::coincident([line1.end, line2.start])
4475}
4476";
4477
4478        let program = Program::parse(initial_source).unwrap().0.unwrap();
4479
4480        let mut frontend = FrontendState::new();
4481
4482        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4483        let mock_ctx = ExecutorContext::new_mock(None).await;
4484        let version = Version(0);
4485
4486        frontend.hack_set_program(&ctx, program).await.unwrap();
4487        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4488        let sketch_id = sketch_object.id;
4489        let sketch = expect_sketch(sketch_object);
4490        let line_id = *sketch.segments.get(5).unwrap();
4491
4492        let (src_delta, scene_delta) = frontend
4493            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4494            .await
4495            .unwrap();
4496        assert_eq!(
4497            src_delta.text.as_str(),
4498            "\
4499@settings(experimentalFeatures = allow)
4500
4501sketch(on = XY) {
4502  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4503}
4504"
4505        );
4506        assert_eq!(
4507            scene_delta.new_graph.objects.len(),
4508            5,
4509            "{:#?}",
4510            scene_delta.new_graph.objects
4511        );
4512
4513        ctx.close().await;
4514        mock_ctx.close().await;
4515    }
4516
4517    #[tokio::test(flavor = "multi_thread")]
4518    async fn test_delete_line_cascades_to_distance_constraint() {
4519        let initial_source = "\
4520@settings(experimentalFeatures = allow)
4521
4522sketch(on = XY) {
4523  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4524  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4525  sketch2::distance([line1.end, line2.start]) == 10mm
4526}
4527";
4528
4529        let program = Program::parse(initial_source).unwrap().0.unwrap();
4530
4531        let mut frontend = FrontendState::new();
4532
4533        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4534        let mock_ctx = ExecutorContext::new_mock(None).await;
4535        let version = Version(0);
4536
4537        frontend.hack_set_program(&ctx, program).await.unwrap();
4538        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4539        let sketch_id = sketch_object.id;
4540        let sketch = expect_sketch(sketch_object);
4541        let line_id = *sketch.segments.get(5).unwrap();
4542
4543        let (src_delta, scene_delta) = frontend
4544            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4545            .await
4546            .unwrap();
4547        assert_eq!(
4548            src_delta.text.as_str(),
4549            "\
4550@settings(experimentalFeatures = allow)
4551
4552sketch(on = XY) {
4553  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4554}
4555"
4556        );
4557        assert_eq!(
4558            scene_delta.new_graph.objects.len(),
4559            5,
4560            "{:#?}",
4561            scene_delta.new_graph.objects
4562        );
4563
4564        ctx.close().await;
4565        mock_ctx.close().await;
4566    }
4567
4568    #[tokio::test(flavor = "multi_thread")]
4569    async fn test_delete_line_line_coincident_constraint() {
4570        let initial_source = "\
4571@settings(experimentalFeatures = allow)
4572
4573sketch(on = XY) {
4574  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4575  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4576  sketch2::coincident([line1, line2])
4577}
4578";
4579
4580        let program = Program::parse(initial_source).unwrap().0.unwrap();
4581
4582        let mut frontend = FrontendState::new();
4583
4584        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4585        let mock_ctx = ExecutorContext::new_mock(None).await;
4586        let version = Version(0);
4587
4588        frontend.hack_set_program(&ctx, program).await.unwrap();
4589        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4590        let sketch_id = sketch_object.id;
4591        let sketch = expect_sketch(sketch_object);
4592
4593        let coincident_id = *sketch.constraints.first().unwrap();
4594
4595        let (src_delta, scene_delta) = frontend
4596            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4597            .await
4598            .unwrap();
4599        assert_eq!(
4600            src_delta.text.as_str(),
4601            "\
4602@settings(experimentalFeatures = allow)
4603
4604sketch(on = XY) {
4605  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4606  line2 = sketch2::line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
4607}
4608"
4609        );
4610        assert_eq!(scene_delta.new_objects, vec![]);
4611        assert_eq!(scene_delta.new_graph.objects.len(), 8);
4612
4613        ctx.close().await;
4614        mock_ctx.close().await;
4615    }
4616
4617    #[tokio::test(flavor = "multi_thread")]
4618    async fn test_two_points_coincident() {
4619        let initial_source = "\
4620@settings(experimentalFeatures = allow)
4621
4622sketch(on = XY) {
4623  point1 = sketch2::point(at = [var 1, var 2])
4624  sketch2::point(at = [3, 4])
4625}
4626";
4627
4628        let program = Program::parse(initial_source).unwrap().0.unwrap();
4629
4630        let mut frontend = FrontendState::new();
4631
4632        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4633        let mock_ctx = ExecutorContext::new_mock(None).await;
4634        let version = Version(0);
4635
4636        frontend.hack_set_program(&ctx, program).await.unwrap();
4637        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4638        let sketch_id = sketch_object.id;
4639        let sketch = expect_sketch(sketch_object);
4640        let point0_id = *sketch.segments.first().unwrap();
4641        let point1_id = *sketch.segments.get(1).unwrap();
4642
4643        let constraint = Constraint::Coincident(Coincident {
4644            segments: vec![point0_id, point1_id],
4645        });
4646        let (src_delta, scene_delta) = frontend
4647            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4648            .await
4649            .unwrap();
4650        assert_eq!(
4651            src_delta.text.as_str(),
4652            "\
4653@settings(experimentalFeatures = allow)
4654
4655sketch(on = XY) {
4656  point1 = sketch2::point(at = [var 1, var 2])
4657  point2 = sketch2::point(at = [3, 4])
4658  sketch2::coincident([point1, point2])
4659}
4660"
4661        );
4662        assert_eq!(
4663            scene_delta.new_graph.objects.len(),
4664            5,
4665            "{:#?}",
4666            scene_delta.new_graph.objects
4667        );
4668
4669        ctx.close().await;
4670        mock_ctx.close().await;
4671    }
4672
4673    #[tokio::test(flavor = "multi_thread")]
4674    async fn test_coincident_of_line_end_points() {
4675        let initial_source = "\
4676@settings(experimentalFeatures = allow)
4677
4678sketch(on = XY) {
4679  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4680  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4681}
4682";
4683
4684        let program = Program::parse(initial_source).unwrap().0.unwrap();
4685
4686        let mut frontend = FrontendState::new();
4687
4688        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4689        let mock_ctx = ExecutorContext::new_mock(None).await;
4690        let version = Version(0);
4691
4692        frontend.hack_set_program(&ctx, program).await.unwrap();
4693        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4694        let sketch_id = sketch_object.id;
4695        let sketch = expect_sketch(sketch_object);
4696        let point0_id = *sketch.segments.get(1).unwrap();
4697        let point1_id = *sketch.segments.get(3).unwrap();
4698
4699        let constraint = Constraint::Coincident(Coincident {
4700            segments: vec![point0_id, point1_id],
4701        });
4702        let (src_delta, scene_delta) = frontend
4703            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4704            .await
4705            .unwrap();
4706        assert_eq!(
4707            src_delta.text.as_str(),
4708            "\
4709@settings(experimentalFeatures = allow)
4710
4711sketch(on = XY) {
4712  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4713  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4714  sketch2::coincident([line1.end, line2.start])
4715}
4716"
4717        );
4718        assert_eq!(
4719            scene_delta.new_graph.objects.len(),
4720            9,
4721            "{:#?}",
4722            scene_delta.new_graph.objects
4723        );
4724
4725        ctx.close().await;
4726        mock_ctx.close().await;
4727    }
4728
4729    #[tokio::test(flavor = "multi_thread")]
4730    async fn test_invalid_coincident_arc_and_line_preserves_state() {
4731        // Test that attempting an invalid coincident constraint (arc and line)
4732        // doesn't corrupt the state, allowing subsequent operations to work.
4733        // This test verifies the transactional fix in add_constraint that prevents
4734        // state corruption when invalid constraints are attempted.
4735        // Example: coincident constraint between an arc segment and a straight line segment
4736        // is geometrically invalid and should fail, but state should remain intact.
4737        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
4738        let program = Program::empty();
4739
4740        let mut frontend = FrontendState::new();
4741        frontend.program = program;
4742
4743        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4744        let mock_ctx = ExecutorContext::new_mock(None).await;
4745        let version = Version(0);
4746
4747        let sketch_args = SketchCtor {
4748            on: PlaneName::Xy.to_string(),
4749        };
4750        let (_src_delta, _scene_delta, sketch_id) = frontend
4751            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4752            .await
4753            .unwrap();
4754
4755        // Add an arc segment
4756        let arc_ctor = ArcCtor {
4757            start: Point2d {
4758                x: Expr::Var(Number {
4759                    value: 0.0,
4760                    units: NumericSuffix::Mm,
4761                }),
4762                y: Expr::Var(Number {
4763                    value: 0.0,
4764                    units: NumericSuffix::Mm,
4765                }),
4766            },
4767            end: Point2d {
4768                x: Expr::Var(Number {
4769                    value: 10.0,
4770                    units: NumericSuffix::Mm,
4771                }),
4772                y: Expr::Var(Number {
4773                    value: 10.0,
4774                    units: NumericSuffix::Mm,
4775                }),
4776            },
4777            center: Point2d {
4778                x: Expr::Var(Number {
4779                    value: 10.0,
4780                    units: NumericSuffix::Mm,
4781                }),
4782                y: Expr::Var(Number {
4783                    value: 0.0,
4784                    units: NumericSuffix::Mm,
4785                }),
4786            },
4787            construction: None,
4788        };
4789        let (_src_delta, scene_delta) = frontend
4790            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
4791            .await
4792            .unwrap();
4793        // The arc is the last object in new_objects (after the 3 points: start, end, center)
4794        let arc_id = *scene_delta.new_objects.last().unwrap();
4795
4796        // Add a line segment
4797        let line_ctor = LineCtor {
4798            start: Point2d {
4799                x: Expr::Var(Number {
4800                    value: 20.0,
4801                    units: NumericSuffix::Mm,
4802                }),
4803                y: Expr::Var(Number {
4804                    value: 0.0,
4805                    units: NumericSuffix::Mm,
4806                }),
4807            },
4808            end: Point2d {
4809                x: Expr::Var(Number {
4810                    value: 30.0,
4811                    units: NumericSuffix::Mm,
4812                }),
4813                y: Expr::Var(Number {
4814                    value: 10.0,
4815                    units: NumericSuffix::Mm,
4816                }),
4817            },
4818            construction: None,
4819        };
4820        let (_src_delta, scene_delta) = frontend
4821            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
4822            .await
4823            .unwrap();
4824        // The line is the last object in new_objects (after the 2 points: start, end)
4825        let line_id = *scene_delta.new_objects.last().unwrap();
4826
4827        // Attempt to add an invalid coincident constraint between arc and line
4828        // This should fail during execution, but state should remain intact
4829        let constraint = Constraint::Coincident(Coincident {
4830            segments: vec![arc_id, line_id],
4831        });
4832        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
4833
4834        // The constraint addition should fail (invalid constraint)
4835        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
4836
4837        // Verify state is not corrupted by checking that we can still access the scene graph
4838        // and that the original segments are still present with their source ranges
4839        let sketch_object_after =
4840            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
4841        let sketch_after = expect_sketch(sketch_object_after);
4842
4843        // Verify both segments are still in the sketch
4844        assert!(
4845            sketch_after.segments.contains(&arc_id),
4846            "Arc segment should still exist after failed constraint"
4847        );
4848        assert!(
4849            sketch_after.segments.contains(&line_id),
4850            "Line segment should still exist after failed constraint"
4851        );
4852
4853        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
4854        let arc_obj = frontend
4855            .scene_graph
4856            .objects
4857            .get(arc_id.0)
4858            .expect("Arc object should still be accessible");
4859        let line_obj = frontend
4860            .scene_graph
4861            .objects
4862            .get(line_id.0)
4863            .expect("Line object should still be accessible");
4864
4865        // Verify source ranges are still valid (not corrupted)
4866        // Just verify that the objects are still accessible and have the expected types
4867        match &arc_obj.kind {
4868            ObjectKind::Segment {
4869                segment: Segment::Arc(_),
4870            } => {}
4871            _ => panic!("Arc object should still be an arc segment"),
4872        }
4873        match &line_obj.kind {
4874            ObjectKind::Segment {
4875                segment: Segment::Line(_),
4876            } => {}
4877            _ => panic!("Line object should still be a line segment"),
4878        }
4879
4880        ctx.close().await;
4881        mock_ctx.close().await;
4882    }
4883
4884    #[tokio::test(flavor = "multi_thread")]
4885    async fn test_distance_two_points() {
4886        let initial_source = "\
4887@settings(experimentalFeatures = allow)
4888
4889sketch(on = XY) {
4890  sketch2::point(at = [var 1, var 2])
4891  sketch2::point(at = [var 3, var 4])
4892}
4893";
4894
4895        let program = Program::parse(initial_source).unwrap().0.unwrap();
4896
4897        let mut frontend = FrontendState::new();
4898
4899        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4900        let mock_ctx = ExecutorContext::new_mock(None).await;
4901        let version = Version(0);
4902
4903        frontend.hack_set_program(&ctx, program).await.unwrap();
4904        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4905        let sketch_id = sketch_object.id;
4906        let sketch = expect_sketch(sketch_object);
4907        let point0_id = *sketch.segments.first().unwrap();
4908        let point1_id = *sketch.segments.get(1).unwrap();
4909
4910        let constraint = Constraint::Distance(Distance {
4911            points: vec![point0_id, point1_id],
4912            distance: Number {
4913                value: 2.0,
4914                units: NumericSuffix::Mm,
4915            },
4916        });
4917        let (src_delta, scene_delta) = frontend
4918            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4919            .await
4920            .unwrap();
4921        assert_eq!(
4922            src_delta.text.as_str(),
4923            // The lack indentation is a formatter bug.
4924            "\
4925@settings(experimentalFeatures = allow)
4926
4927sketch(on = XY) {
4928  point1 = sketch2::point(at = [var 1, var 2])
4929  point2 = sketch2::point(at = [var 3, var 4])
4930sketch2::distance([point1, point2]) == 2mm
4931}
4932"
4933        );
4934        assert_eq!(
4935            scene_delta.new_graph.objects.len(),
4936            5,
4937            "{:#?}",
4938            scene_delta.new_graph.objects
4939        );
4940
4941        ctx.close().await;
4942        mock_ctx.close().await;
4943    }
4944
4945    #[tokio::test(flavor = "multi_thread")]
4946    async fn test_horizontal_distance_two_points() {
4947        let initial_source = "\
4948@settings(experimentalFeatures = allow)
4949
4950sketch(on = XY) {
4951  sketch2::point(at = [var 1, var 2])
4952  sketch2::point(at = [var 3, var 4])
4953}
4954";
4955
4956        let program = Program::parse(initial_source).unwrap().0.unwrap();
4957
4958        let mut frontend = FrontendState::new();
4959
4960        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4961        let mock_ctx = ExecutorContext::new_mock(None).await;
4962        let version = Version(0);
4963
4964        frontend.hack_set_program(&ctx, program).await.unwrap();
4965        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4966        let sketch_id = sketch_object.id;
4967        let sketch = expect_sketch(sketch_object);
4968        let point0_id = *sketch.segments.first().unwrap();
4969        let point1_id = *sketch.segments.get(1).unwrap();
4970
4971        let constraint = Constraint::HorizontalDistance(Distance {
4972            points: vec![point0_id, point1_id],
4973            distance: Number {
4974                value: 2.0,
4975                units: NumericSuffix::Mm,
4976            },
4977        });
4978        let (src_delta, scene_delta) = frontend
4979            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4980            .await
4981            .unwrap();
4982        assert_eq!(
4983            src_delta.text.as_str(),
4984            // The lack indentation is a formatter bug.
4985            "\
4986@settings(experimentalFeatures = allow)
4987
4988sketch(on = XY) {
4989  point1 = sketch2::point(at = [var 1, var 2])
4990  point2 = sketch2::point(at = [var 3, var 4])
4991sketch2::horizontalDistance([point1, point2]) == 2mm
4992}
4993"
4994        );
4995        assert_eq!(
4996            scene_delta.new_graph.objects.len(),
4997            5,
4998            "{:#?}",
4999            scene_delta.new_graph.objects
5000        );
5001
5002        ctx.close().await;
5003        mock_ctx.close().await;
5004    }
5005
5006    #[tokio::test(flavor = "multi_thread")]
5007    async fn test_radius_single_arc_segment() {
5008        let initial_source = "\
5009@settings(experimentalFeatures = allow)
5010
5011sketch(on = XY) {
5012  sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5013}
5014";
5015
5016        let program = Program::parse(initial_source).unwrap().0.unwrap();
5017
5018        let mut frontend = FrontendState::new();
5019
5020        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5021        let mock_ctx = ExecutorContext::new_mock(None).await;
5022        let version = Version(0);
5023
5024        frontend.hack_set_program(&ctx, program).await.unwrap();
5025        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5026        let sketch_id = sketch_object.id;
5027        let sketch = expect_sketch(sketch_object);
5028        // Find the arc segment (not the points)
5029        let arc_id = sketch
5030            .segments
5031            .iter()
5032            .find(|&seg_id| {
5033                let obj = frontend.scene_graph.objects.get(seg_id.0);
5034                matches!(
5035                    obj.map(|o| &o.kind),
5036                    Some(ObjectKind::Segment {
5037                        segment: Segment::Arc(_)
5038                    })
5039                )
5040            })
5041            .unwrap();
5042
5043        let constraint = Constraint::Radius(Radius {
5044            arc: *arc_id,
5045            radius: Number {
5046                value: 5.0,
5047                units: NumericSuffix::Mm,
5048            },
5049        });
5050        let (src_delta, scene_delta) = frontend
5051            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5052            .await
5053            .unwrap();
5054        assert_eq!(
5055            src_delta.text.as_str(),
5056            // The lack indentation is a formatter bug.
5057            "\
5058@settings(experimentalFeatures = allow)
5059
5060sketch(on = XY) {
5061  arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5062sketch2::radius(arc1) == 5mm
5063}
5064"
5065        );
5066        assert_eq!(
5067            scene_delta.new_graph.objects.len(),
5068            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
5069            "{:#?}",
5070            scene_delta.new_graph.objects
5071        );
5072
5073        ctx.close().await;
5074        mock_ctx.close().await;
5075    }
5076
5077    #[tokio::test(flavor = "multi_thread")]
5078    async fn test_vertical_distance_two_points() {
5079        let initial_source = "\
5080@settings(experimentalFeatures = allow)
5081
5082sketch(on = XY) {
5083  sketch2::point(at = [var 1, var 2])
5084  sketch2::point(at = [var 3, var 4])
5085}
5086";
5087
5088        let program = Program::parse(initial_source).unwrap().0.unwrap();
5089
5090        let mut frontend = FrontendState::new();
5091
5092        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5093        let mock_ctx = ExecutorContext::new_mock(None).await;
5094        let version = Version(0);
5095
5096        frontend.hack_set_program(&ctx, program).await.unwrap();
5097        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5098        let sketch_id = sketch_object.id;
5099        let sketch = expect_sketch(sketch_object);
5100        let point0_id = *sketch.segments.first().unwrap();
5101        let point1_id = *sketch.segments.get(1).unwrap();
5102
5103        let constraint = Constraint::VerticalDistance(Distance {
5104            points: vec![point0_id, point1_id],
5105            distance: Number {
5106                value: 2.0,
5107                units: NumericSuffix::Mm,
5108            },
5109        });
5110        let (src_delta, scene_delta) = frontend
5111            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5112            .await
5113            .unwrap();
5114        assert_eq!(
5115            src_delta.text.as_str(),
5116            // The lack indentation is a formatter bug.
5117            "\
5118@settings(experimentalFeatures = allow)
5119
5120sketch(on = XY) {
5121  point1 = sketch2::point(at = [var 1, var 2])
5122  point2 = sketch2::point(at = [var 3, var 4])
5123sketch2::verticalDistance([point1, point2]) == 2mm
5124}
5125"
5126        );
5127        assert_eq!(
5128            scene_delta.new_graph.objects.len(),
5129            5,
5130            "{:#?}",
5131            scene_delta.new_graph.objects
5132        );
5133
5134        ctx.close().await;
5135        mock_ctx.close().await;
5136    }
5137
5138    #[tokio::test(flavor = "multi_thread")]
5139    async fn test_radius_error_cases() {
5140        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5141        let mock_ctx = ExecutorContext::new_mock(None).await;
5142        let version = Version(0);
5143
5144        // Test: Single point should error
5145        let initial_source_point = "\
5146@settings(experimentalFeatures = allow)
5147
5148sketch(on = XY) {
5149  sketch2::point(at = [var 1, var 2])
5150}
5151";
5152        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5153        let mut frontend_point = FrontendState::new();
5154        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5155        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5156        let sketch_id_point = sketch_object_point.id;
5157        let sketch_point = expect_sketch(sketch_object_point);
5158        let point_id = *sketch_point.segments.first().unwrap();
5159
5160        let constraint_point = Constraint::Radius(Radius {
5161            arc: point_id,
5162            radius: Number {
5163                value: 5.0,
5164                units: NumericSuffix::Mm,
5165            },
5166        });
5167        let result_point = frontend_point
5168            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5169            .await;
5170        assert!(result_point.is_err(), "Single point should error for radius");
5171
5172        // Test: Single line segment should error (only arc segments supported)
5173        let initial_source_line = "\
5174@settings(experimentalFeatures = allow)
5175
5176sketch(on = XY) {
5177  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5178}
5179";
5180        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5181        let mut frontend_line = FrontendState::new();
5182        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5183        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5184        let sketch_id_line = sketch_object_line.id;
5185        let sketch_line = expect_sketch(sketch_object_line);
5186        let line_id = *sketch_line.segments.first().unwrap();
5187
5188        let constraint_line = Constraint::Radius(Radius {
5189            arc: line_id,
5190            radius: Number {
5191                value: 5.0,
5192                units: NumericSuffix::Mm,
5193            },
5194        });
5195        let result_line = frontend_line
5196            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5197            .await;
5198        assert!(result_line.is_err(), "Single line segment should error for radius");
5199
5200        ctx.close().await;
5201        mock_ctx.close().await;
5202    }
5203
5204    #[tokio::test(flavor = "multi_thread")]
5205    async fn test_diameter_single_arc_segment() {
5206        let initial_source = "\
5207@settings(experimentalFeatures = allow)
5208
5209sketch(on = XY) {
5210  sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5211}
5212";
5213
5214        let program = Program::parse(initial_source).unwrap().0.unwrap();
5215
5216        let mut frontend = FrontendState::new();
5217
5218        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5219        let mock_ctx = ExecutorContext::new_mock(None).await;
5220        let version = Version(0);
5221
5222        frontend.hack_set_program(&ctx, program).await.unwrap();
5223        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5224        let sketch_id = sketch_object.id;
5225        let sketch = expect_sketch(sketch_object);
5226        // Find the arc segment (not the points)
5227        let arc_id = sketch
5228            .segments
5229            .iter()
5230            .find(|&seg_id| {
5231                let obj = frontend.scene_graph.objects.get(seg_id.0);
5232                matches!(
5233                    obj.map(|o| &o.kind),
5234                    Some(ObjectKind::Segment {
5235                        segment: Segment::Arc(_)
5236                    })
5237                )
5238            })
5239            .unwrap();
5240
5241        let constraint = Constraint::Diameter(Diameter {
5242            arc: *arc_id,
5243            diameter: Number {
5244                value: 10.0,
5245                units: NumericSuffix::Mm,
5246            },
5247        });
5248        let (src_delta, scene_delta) = frontend
5249            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5250            .await
5251            .unwrap();
5252        assert_eq!(
5253            src_delta.text.as_str(),
5254            // The lack indentation is a formatter bug.
5255            "\
5256@settings(experimentalFeatures = allow)
5257
5258sketch(on = XY) {
5259  arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5260sketch2::diameter(arc1) == 10mm
5261}
5262"
5263        );
5264        assert_eq!(
5265            scene_delta.new_graph.objects.len(),
5266            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
5267            "{:#?}",
5268            scene_delta.new_graph.objects
5269        );
5270
5271        ctx.close().await;
5272        mock_ctx.close().await;
5273    }
5274
5275    #[tokio::test(flavor = "multi_thread")]
5276    async fn test_diameter_error_cases() {
5277        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5278        let mock_ctx = ExecutorContext::new_mock(None).await;
5279        let version = Version(0);
5280
5281        // Test: Single point should error
5282        let initial_source_point = "\
5283@settings(experimentalFeatures = allow)
5284
5285sketch(on = XY) {
5286  sketch2::point(at = [var 1, var 2])
5287}
5288";
5289        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5290        let mut frontend_point = FrontendState::new();
5291        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5292        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5293        let sketch_id_point = sketch_object_point.id;
5294        let sketch_point = expect_sketch(sketch_object_point);
5295        let point_id = *sketch_point.segments.first().unwrap();
5296
5297        let constraint_point = Constraint::Diameter(Diameter {
5298            arc: point_id,
5299            diameter: Number {
5300                value: 10.0,
5301                units: NumericSuffix::Mm,
5302            },
5303        });
5304        let result_point = frontend_point
5305            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5306            .await;
5307        assert!(result_point.is_err(), "Single point should error for diameter");
5308
5309        // Test: Single line segment should error (only arc segments supported)
5310        let initial_source_line = "\
5311@settings(experimentalFeatures = allow)
5312
5313sketch(on = XY) {
5314  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5315}
5316";
5317        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5318        let mut frontend_line = FrontendState::new();
5319        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5320        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5321        let sketch_id_line = sketch_object_line.id;
5322        let sketch_line = expect_sketch(sketch_object_line);
5323        let line_id = *sketch_line.segments.first().unwrap();
5324
5325        let constraint_line = Constraint::Diameter(Diameter {
5326            arc: line_id,
5327            diameter: Number {
5328                value: 10.0,
5329                units: NumericSuffix::Mm,
5330            },
5331        });
5332        let result_line = frontend_line
5333            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5334            .await;
5335        assert!(result_line.is_err(), "Single line segment should error for diameter");
5336
5337        ctx.close().await;
5338        mock_ctx.close().await;
5339    }
5340
5341    #[tokio::test(flavor = "multi_thread")]
5342    async fn test_line_horizontal() {
5343        let initial_source = "\
5344@settings(experimentalFeatures = allow)
5345
5346sketch(on = XY) {
5347  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5348}
5349";
5350
5351        let program = Program::parse(initial_source).unwrap().0.unwrap();
5352
5353        let mut frontend = FrontendState::new();
5354
5355        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5356        let mock_ctx = ExecutorContext::new_mock(None).await;
5357        let version = Version(0);
5358
5359        frontend.hack_set_program(&ctx, program).await.unwrap();
5360        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5361        let sketch_id = sketch_object.id;
5362        let sketch = expect_sketch(sketch_object);
5363        let line1_id = *sketch.segments.get(2).unwrap();
5364
5365        let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
5366        let (src_delta, scene_delta) = frontend
5367            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5368            .await
5369            .unwrap();
5370        assert_eq!(
5371            src_delta.text.as_str(),
5372            "\
5373@settings(experimentalFeatures = allow)
5374
5375sketch(on = XY) {
5376  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5377  sketch2::horizontal(line1)
5378}
5379"
5380        );
5381        assert_eq!(
5382            scene_delta.new_graph.objects.len(),
5383            6,
5384            "{:#?}",
5385            scene_delta.new_graph.objects
5386        );
5387
5388        ctx.close().await;
5389        mock_ctx.close().await;
5390    }
5391
5392    #[tokio::test(flavor = "multi_thread")]
5393    async fn test_line_vertical() {
5394        let initial_source = "\
5395@settings(experimentalFeatures = allow)
5396
5397sketch(on = XY) {
5398  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5399}
5400";
5401
5402        let program = Program::parse(initial_source).unwrap().0.unwrap();
5403
5404        let mut frontend = FrontendState::new();
5405
5406        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5407        let mock_ctx = ExecutorContext::new_mock(None).await;
5408        let version = Version(0);
5409
5410        frontend.hack_set_program(&ctx, program).await.unwrap();
5411        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5412        let sketch_id = sketch_object.id;
5413        let sketch = expect_sketch(sketch_object);
5414        let line1_id = *sketch.segments.get(2).unwrap();
5415
5416        let constraint = Constraint::Vertical(Vertical { line: line1_id });
5417        let (src_delta, scene_delta) = frontend
5418            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5419            .await
5420            .unwrap();
5421        assert_eq!(
5422            src_delta.text.as_str(),
5423            "\
5424@settings(experimentalFeatures = allow)
5425
5426sketch(on = XY) {
5427  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5428  sketch2::vertical(line1)
5429}
5430"
5431        );
5432        assert_eq!(
5433            scene_delta.new_graph.objects.len(),
5434            6,
5435            "{:#?}",
5436            scene_delta.new_graph.objects
5437        );
5438
5439        ctx.close().await;
5440        mock_ctx.close().await;
5441    }
5442
5443    #[tokio::test(flavor = "multi_thread")]
5444    async fn test_lines_equal_length() {
5445        let initial_source = "\
5446@settings(experimentalFeatures = allow)
5447
5448sketch(on = XY) {
5449  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5450  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5451}
5452";
5453
5454        let program = Program::parse(initial_source).unwrap().0.unwrap();
5455
5456        let mut frontend = FrontendState::new();
5457
5458        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5459        let mock_ctx = ExecutorContext::new_mock(None).await;
5460        let version = Version(0);
5461
5462        frontend.hack_set_program(&ctx, program).await.unwrap();
5463        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5464        let sketch_id = sketch_object.id;
5465        let sketch = expect_sketch(sketch_object);
5466        let line1_id = *sketch.segments.get(2).unwrap();
5467        let line2_id = *sketch.segments.get(5).unwrap();
5468
5469        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
5470            lines: vec![line1_id, line2_id],
5471        });
5472        let (src_delta, scene_delta) = frontend
5473            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5474            .await
5475            .unwrap();
5476        assert_eq!(
5477            src_delta.text.as_str(),
5478            "\
5479@settings(experimentalFeatures = allow)
5480
5481sketch(on = XY) {
5482  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5483  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5484  sketch2::equalLength([line1, line2])
5485}
5486"
5487        );
5488        assert_eq!(
5489            scene_delta.new_graph.objects.len(),
5490            9,
5491            "{:#?}",
5492            scene_delta.new_graph.objects
5493        );
5494
5495        ctx.close().await;
5496        mock_ctx.close().await;
5497    }
5498
5499    #[tokio::test(flavor = "multi_thread")]
5500    async fn test_lines_parallel() {
5501        let initial_source = "\
5502@settings(experimentalFeatures = allow)
5503
5504sketch(on = XY) {
5505  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5506  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5507}
5508";
5509
5510        let program = Program::parse(initial_source).unwrap().0.unwrap();
5511
5512        let mut frontend = FrontendState::new();
5513
5514        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5515        let mock_ctx = ExecutorContext::new_mock(None).await;
5516        let version = Version(0);
5517
5518        frontend.hack_set_program(&ctx, program).await.unwrap();
5519        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5520        let sketch_id = sketch_object.id;
5521        let sketch = expect_sketch(sketch_object);
5522        let line1_id = *sketch.segments.get(2).unwrap();
5523        let line2_id = *sketch.segments.get(5).unwrap();
5524
5525        let constraint = Constraint::Parallel(Parallel {
5526            lines: vec![line1_id, line2_id],
5527        });
5528        let (src_delta, scene_delta) = frontend
5529            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5530            .await
5531            .unwrap();
5532        assert_eq!(
5533            src_delta.text.as_str(),
5534            "\
5535@settings(experimentalFeatures = allow)
5536
5537sketch(on = XY) {
5538  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5539  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5540  sketch2::parallel([line1, line2])
5541}
5542"
5543        );
5544        assert_eq!(
5545            scene_delta.new_graph.objects.len(),
5546            9,
5547            "{:#?}",
5548            scene_delta.new_graph.objects
5549        );
5550
5551        ctx.close().await;
5552        mock_ctx.close().await;
5553    }
5554
5555    #[tokio::test(flavor = "multi_thread")]
5556    async fn test_lines_perpendicular() {
5557        let initial_source = "\
5558@settings(experimentalFeatures = allow)
5559
5560sketch(on = XY) {
5561  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5562  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5563}
5564";
5565
5566        let program = Program::parse(initial_source).unwrap().0.unwrap();
5567
5568        let mut frontend = FrontendState::new();
5569
5570        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5571        let mock_ctx = ExecutorContext::new_mock(None).await;
5572        let version = Version(0);
5573
5574        frontend.hack_set_program(&ctx, program).await.unwrap();
5575        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5576        let sketch_id = sketch_object.id;
5577        let sketch = expect_sketch(sketch_object);
5578        let line1_id = *sketch.segments.get(2).unwrap();
5579        let line2_id = *sketch.segments.get(5).unwrap();
5580
5581        let constraint = Constraint::Perpendicular(Perpendicular {
5582            lines: vec![line1_id, line2_id],
5583        });
5584        let (src_delta, scene_delta) = frontend
5585            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5586            .await
5587            .unwrap();
5588        assert_eq!(
5589            src_delta.text.as_str(),
5590            "\
5591@settings(experimentalFeatures = allow)
5592
5593sketch(on = XY) {
5594  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5595  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5596  sketch2::perpendicular([line1, line2])
5597}
5598"
5599        );
5600        assert_eq!(
5601            scene_delta.new_graph.objects.len(),
5602            9,
5603            "{:#?}",
5604            scene_delta.new_graph.objects
5605        );
5606
5607        ctx.close().await;
5608        mock_ctx.close().await;
5609    }
5610
5611    #[tokio::test(flavor = "multi_thread")]
5612    async fn test_sketch_on_face_simple() {
5613        let initial_source = "\
5614@settings(experimentalFeatures = allow)
5615
5616len = 2mm
5617cube = startSketchOn(XY)
5618  |> startProfile(at = [0, 0])
5619  |> line(end = [len, 0], tag = $side)
5620  |> line(end = [0, len])
5621  |> line(end = [-len, 0])
5622  |> line(end = [0, -len])
5623  |> close()
5624  |> extrude(length = len)
5625
5626face = faceOf(cube, face = side)
5627";
5628
5629        let program = Program::parse(initial_source).unwrap().0.unwrap();
5630
5631        let mut frontend = FrontendState::new();
5632
5633        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5634        let mock_ctx = ExecutorContext::new_mock(None).await;
5635        let version = Version(0);
5636
5637        frontend.hack_set_program(&ctx, program).await.unwrap();
5638        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
5639        let face_id = face_object.id;
5640
5641        let sketch_args = SketchCtor { on: "face".to_owned() };
5642        let (_src_delta, scene_delta, sketch_id) = frontend
5643            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5644            .await
5645            .unwrap();
5646        assert_eq!(sketch_id, ObjectId(2));
5647        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5648        let sketch_object = &scene_delta.new_graph.objects[2];
5649        assert_eq!(sketch_object.id, ObjectId(2));
5650        assert_eq!(
5651            sketch_object.kind,
5652            ObjectKind::Sketch(Sketch {
5653                args: SketchCtor { on: "face".to_owned() },
5654                plane: face_id,
5655                segments: vec![],
5656                constraints: vec![],
5657            })
5658        );
5659        assert_eq!(scene_delta.new_graph.objects.len(), 3);
5660
5661        ctx.close().await;
5662        mock_ctx.close().await;
5663    }
5664
5665    #[tokio::test(flavor = "multi_thread")]
5666    async fn test_sketch_on_plane_incremental() {
5667        let initial_source = "\
5668@settings(experimentalFeatures = allow)
5669
5670len = 2mm
5671cube = startSketchOn(XY)
5672  |> startProfile(at = [0, 0])
5673  |> line(end = [len, 0], tag = $side)
5674  |> line(end = [0, len])
5675  |> line(end = [-len, 0])
5676  |> line(end = [0, -len])
5677  |> close()
5678  |> extrude(length = len)
5679
5680plane = planeOf(cube, face = side)
5681";
5682
5683        let program = Program::parse(initial_source).unwrap().0.unwrap();
5684
5685        let mut frontend = FrontendState::new();
5686
5687        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5688        let mock_ctx = ExecutorContext::new_mock(None).await;
5689        let version = Version(0);
5690
5691        frontend.hack_set_program(&ctx, program).await.unwrap();
5692        // Find the last plane since the first plane is the XY plane.
5693        let plane_object = frontend
5694            .scene_graph
5695            .objects
5696            .iter()
5697            .rev()
5698            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
5699            .unwrap();
5700        let plane_id = plane_object.id;
5701
5702        let sketch_args = SketchCtor { on: "plane".to_owned() };
5703        let (src_delta, scene_delta, sketch_id) = frontend
5704            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5705            .await
5706            .unwrap();
5707        assert_eq!(
5708            src_delta.text.as_str(),
5709            "\
5710@settings(experimentalFeatures = allow)
5711
5712len = 2mm
5713cube = startSketchOn(XY)
5714  |> startProfile(at = [0, 0])
5715  |> line(end = [len, 0], tag = $side)
5716  |> line(end = [0, len])
5717  |> line(end = [-len, 0])
5718  |> line(end = [0, -len])
5719  |> close()
5720  |> extrude(length = len)
5721
5722plane = planeOf(cube, face = side)
5723sketch(on = plane) {
5724}
5725"
5726        );
5727        assert_eq!(sketch_id, ObjectId(2));
5728        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5729        let sketch_object = &scene_delta.new_graph.objects[2];
5730        assert_eq!(sketch_object.id, ObjectId(2));
5731        assert_eq!(
5732            sketch_object.kind,
5733            ObjectKind::Sketch(Sketch {
5734                args: SketchCtor { on: "plane".to_owned() },
5735                plane: plane_id,
5736                segments: vec![],
5737                constraints: vec![],
5738            })
5739        );
5740        assert_eq!(scene_delta.new_graph.objects.len(), 3);
5741
5742        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
5743        assert_eq!(plane_object.id, plane_id);
5744        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
5745
5746        ctx.close().await;
5747        mock_ctx.close().await;
5748    }
5749
5750    #[tokio::test(flavor = "multi_thread")]
5751    async fn test_multiple_sketch_blocks() {
5752        let initial_source = "\
5753@settings(experimentalFeatures = allow)
5754
5755// Cube that requires the engine.
5756width = 2
5757sketch001 = startSketchOn(XY)
5758profile001 = startProfile(sketch001, at = [0, 0])
5759  |> yLine(length = width, tag = $seg1)
5760  |> xLine(length = width)
5761  |> yLine(length = -width)
5762  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5763  |> close()
5764extrude001 = extrude(profile001, length = width)
5765
5766// Get a value that requires the engine.
5767x = segLen(seg1)
5768
5769// Triangle with side length 2*x.
5770sketch(on = XY) {
5771  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5772  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5773  sketch2::coincident([line1.end, line2.start])
5774  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5775  sketch2::coincident([line2.end, line3.start])
5776  sketch2::coincident([line3.end, line1.start])
5777  sketch2::equalLength([line3, line1])
5778  sketch2::equalLength([line1, line2])
5779sketch2::distance([line1.start, line1.end]) == 2*x
5780}
5781
5782// Line segment with length x.
5783sketch2 = sketch(on = XY) {
5784  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5785sketch2::distance([line1.start, line1.end]) == x
5786}
5787";
5788
5789        let program = Program::parse(initial_source).unwrap().0.unwrap();
5790
5791        let mut frontend = FrontendState::new();
5792
5793        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5794        let mock_ctx = ExecutorContext::new_mock(None).await;
5795        let version = Version(0);
5796        let project_id = ProjectId(0);
5797        let file_id = FileId(0);
5798
5799        frontend.hack_set_program(&ctx, program).await.unwrap();
5800        let sketch_objects = frontend
5801            .scene_graph
5802            .objects
5803            .iter()
5804            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
5805            .collect::<Vec<_>>();
5806        let sketch1_id = sketch_objects.first().unwrap().id;
5807        let sketch2_id = sketch_objects.get(1).unwrap().id;
5808        // First point in sketch1.
5809        let point1_id = ObjectId(sketch1_id.0 + 1);
5810        // First point in sketch2.
5811        let point2_id = ObjectId(sketch2_id.0 + 1);
5812
5813        // Edit the first sketch. Objects before the sketch block should be
5814        // present from execution cache so that we can sketch on prior planes,
5815        // for example. Objects after the first sketch block should not be
5816        // present since those statements are skipped in sketch mode.
5817        //
5818        // - startSketchOn(XY) Plane 1
5819        // - sketch on=XY Plane 1
5820        // - Sketch block 16
5821        let scene_delta = frontend
5822            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
5823            .await
5824            .unwrap();
5825        assert_eq!(
5826            scene_delta.new_graph.objects.len(),
5827            18,
5828            "{:#?}",
5829            scene_delta.new_graph.objects
5830        );
5831
5832        // Edit a point in the first sketch.
5833        let point_ctor = PointCtor {
5834            position: Point2d {
5835                x: Expr::Var(Number {
5836                    value: 1.0,
5837                    units: NumericSuffix::Mm,
5838                }),
5839                y: Expr::Var(Number {
5840                    value: 2.0,
5841                    units: NumericSuffix::Mm,
5842                }),
5843            },
5844        };
5845        let segments = vec![ExistingSegmentCtor {
5846            id: point1_id,
5847            ctor: SegmentCtor::Point(point_ctor),
5848        }];
5849        let (src_delta, _) = frontend
5850            .edit_segments(&mock_ctx, version, sketch1_id, segments)
5851            .await
5852            .unwrap();
5853        // Only the first sketch block changes.
5854        assert_eq!(
5855            src_delta.text.as_str(),
5856            "\
5857@settings(experimentalFeatures = allow)
5858
5859// Cube that requires the engine.
5860width = 2
5861sketch001 = startSketchOn(XY)
5862profile001 = startProfile(sketch001, at = [0, 0])
5863  |> yLine(length = width, tag = $seg1)
5864  |> xLine(length = width)
5865  |> yLine(length = -width)
5866  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5867  |> close()
5868extrude001 = extrude(profile001, length = width)
5869
5870// Get a value that requires the engine.
5871x = segLen(seg1)
5872
5873// Triangle with side length 2*x.
5874sketch(on = XY) {
5875  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
5876  line2 = sketch2::line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
5877  sketch2::coincident([line1.end, line2.start])
5878  line3 = sketch2::line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
5879  sketch2::coincident([line2.end, line3.start])
5880  sketch2::coincident([line3.end, line1.start])
5881  sketch2::equalLength([line3, line1])
5882  sketch2::equalLength([line1, line2])
5883sketch2::distance([line1.start, line1.end]) == 2 * x
5884}
5885
5886// Line segment with length x.
5887sketch2 = sketch(on = XY) {
5888  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5889sketch2::distance([line1.start, line1.end]) == x
5890}
5891"
5892        );
5893
5894        // Execute mock to simulate drag end.
5895        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
5896        // Only the first sketch block changes.
5897        assert_eq!(
5898            src_delta.text.as_str(),
5899            "\
5900@settings(experimentalFeatures = allow)
5901
5902// Cube that requires the engine.
5903width = 2
5904sketch001 = startSketchOn(XY)
5905profile001 = startProfile(sketch001, at = [0, 0])
5906  |> yLine(length = width, tag = $seg1)
5907  |> xLine(length = width)
5908  |> yLine(length = -width)
5909  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5910  |> close()
5911extrude001 = extrude(profile001, length = width)
5912
5913// Get a value that requires the engine.
5914x = segLen(seg1)
5915
5916// Triangle with side length 2*x.
5917sketch(on = XY) {
5918  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
5919  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5920  sketch2::coincident([line1.end, line2.start])
5921  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5922  sketch2::coincident([line2.end, line3.start])
5923  sketch2::coincident([line3.end, line1.start])
5924  sketch2::equalLength([line3, line1])
5925  sketch2::equalLength([line1, line2])
5926sketch2::distance([line1.start, line1.end]) == 2 * x
5927}
5928
5929// Line segment with length x.
5930sketch2 = sketch(on = XY) {
5931  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5932sketch2::distance([line1.start, line1.end]) == x
5933}
5934"
5935        );
5936        // Exit sketch. Objects from the entire program should be present.
5937        //
5938        // - startSketchOn(XY) Plane 1
5939        // - sketch on=XY Plane 1
5940        // - Sketch block 16
5941        // - sketch on=XY cached
5942        // - Sketch block 5
5943        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
5944        assert_eq!(scene.objects.len(), 23, "{:#?}", scene.objects);
5945
5946        // Edit the second sketch.
5947        //
5948        // - startSketchOn(XY) Plane 1
5949        // - sketch on=XY Plane 1
5950        // - Sketch block 16
5951        // - sketch on=XY cached
5952        // - Sketch block 5
5953        let scene_delta = frontend
5954            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
5955            .await
5956            .unwrap();
5957        assert_eq!(
5958            scene_delta.new_graph.objects.len(),
5959            23,
5960            "{:#?}",
5961            scene_delta.new_graph.objects
5962        );
5963
5964        // Edit a point in the second sketch.
5965        let point_ctor = PointCtor {
5966            position: Point2d {
5967                x: Expr::Var(Number {
5968                    value: 3.0,
5969                    units: NumericSuffix::Mm,
5970                }),
5971                y: Expr::Var(Number {
5972                    value: 4.0,
5973                    units: NumericSuffix::Mm,
5974                }),
5975            },
5976        };
5977        let segments = vec![ExistingSegmentCtor {
5978            id: point2_id,
5979            ctor: SegmentCtor::Point(point_ctor),
5980        }];
5981        let (src_delta, _) = frontend
5982            .edit_segments(&mock_ctx, version, sketch2_id, segments)
5983            .await
5984            .unwrap();
5985        // Only the second sketch block changes.
5986        assert_eq!(
5987            src_delta.text.as_str(),
5988            "\
5989@settings(experimentalFeatures = allow)
5990
5991// Cube that requires the engine.
5992width = 2
5993sketch001 = startSketchOn(XY)
5994profile001 = startProfile(sketch001, at = [0, 0])
5995  |> yLine(length = width, tag = $seg1)
5996  |> xLine(length = width)
5997  |> yLine(length = -width)
5998  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5999  |> close()
6000extrude001 = extrude(profile001, length = width)
6001
6002// Get a value that requires the engine.
6003x = segLen(seg1)
6004
6005// Triangle with side length 2*x.
6006sketch(on = XY) {
6007  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6008  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6009  sketch2::coincident([line1.end, line2.start])
6010  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6011  sketch2::coincident([line2.end, line3.start])
6012  sketch2::coincident([line3.end, line1.start])
6013  sketch2::equalLength([line3, line1])
6014  sketch2::equalLength([line1, line2])
6015sketch2::distance([line1.start, line1.end]) == 2 * x
6016}
6017
6018// Line segment with length x.
6019sketch2 = sketch(on = XY) {
6020  line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
6021sketch2::distance([line1.start, line1.end]) == x
6022}
6023"
6024        );
6025
6026        // Execute mock to simulate drag end.
6027        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
6028        // Only the second sketch block changes.
6029        assert_eq!(
6030            src_delta.text.as_str(),
6031            "\
6032@settings(experimentalFeatures = allow)
6033
6034// Cube that requires the engine.
6035width = 2
6036sketch001 = startSketchOn(XY)
6037profile001 = startProfile(sketch001, at = [0, 0])
6038  |> yLine(length = width, tag = $seg1)
6039  |> xLine(length = width)
6040  |> yLine(length = -width)
6041  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6042  |> close()
6043extrude001 = extrude(profile001, length = width)
6044
6045// Get a value that requires the engine.
6046x = segLen(seg1)
6047
6048// Triangle with side length 2*x.
6049sketch(on = XY) {
6050  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6051  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6052  sketch2::coincident([line1.end, line2.start])
6053  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6054  sketch2::coincident([line2.end, line3.start])
6055  sketch2::coincident([line3.end, line1.start])
6056  sketch2::equalLength([line3, line1])
6057  sketch2::equalLength([line1, line2])
6058sketch2::distance([line1.start, line1.end]) == 2 * x
6059}
6060
6061// Line segment with length x.
6062sketch2 = sketch(on = XY) {
6063  line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
6064sketch2::distance([line1.start, line1.end]) == x
6065}
6066"
6067        );
6068
6069        ctx.close().await;
6070        mock_ctx.close().await;
6071    }
6072}