kcl_lib/
frontend.rs

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