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