kcl_lib/
frontend.rs

1use std::{cell::Cell, collections::HashSet, ops::ControlFlow};
2
3use kcl_error::SourceRange;
4
5use crate::{
6    ExecOutcome, ExecutorContext, Program,
7    collections::AhashIndexSet,
8    exec::WarningLevel,
9    execution::MockConfig,
10    fmt::format_number_literal,
11    front::{ArcCtor, Distance, Line, LinesEqualLength, Parallel, Perpendicular, PointCtor},
12    frontend::{
13        api::{
14            Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
15            SourceRef, Version,
16        },
17        modify::{find_defined_names, next_free_name},
18        sketch::{
19            Coincident, Constraint, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Segment, SegmentCtor,
20            SketchApi, SketchArgs, Vertical,
21        },
22        traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
23    },
24    parsing::ast::types as ast,
25    std::constraints::LinesAtAngleKind,
26    walk::{NodeMut, Visitable},
27};
28
29pub(crate) mod api;
30mod modify;
31pub(crate) mod sketch;
32mod traverse;
33
34const POINT_FN: &str = "point";
35const POINT_AT_PARAM: &str = "at";
36const LINE_FN: &str = "line";
37const LINE_START_PARAM: &str = "start";
38const LINE_END_PARAM: &str = "end";
39const ARC_FN: &str = "arc";
40const ARC_START_PARAM: &str = "start";
41const ARC_END_PARAM: &str = "end";
42const ARC_CENTER_PARAM: &str = "center";
43
44const COINCIDENT_FN: &str = "coincident";
45const DISTANCE_FN: &str = "distance";
46const EQUAL_LENGTH_FN: &str = "equalLength";
47const HORIZONTAL_FN: &str = "horizontal";
48const VERTICAL_FN: &str = "vertical";
49
50const LINE_PROPERTY_START: &str = "start";
51const LINE_PROPERTY_END: &str = "end";
52
53#[derive(Debug, Clone)]
54pub struct FrontendState {
55    program: Program,
56    scene_graph: SceneGraph,
57}
58
59impl Default for FrontendState {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl FrontendState {
66    pub fn new() -> Self {
67        Self {
68            program: Program::empty(),
69            scene_graph: SceneGraph {
70                project: ProjectId(0),
71                file: FileId(0),
72                version: Version(0),
73                objects: Default::default(),
74                settings: Default::default(),
75                sketch_mode: Default::default(),
76            },
77        }
78    }
79}
80
81impl SketchApi for FrontendState {
82    async fn execute_mock(
83        &mut self,
84        ctx: &ExecutorContext,
85        _version: Version,
86        _sketch: ObjectId,
87    ) -> api::Result<(SceneGraph, ExecOutcome)> {
88        // Execute.
89        let outcome = ctx
90            .run_mock(&self.program, &MockConfig::default())
91            .await
92            .map_err(|err| Error {
93                msg: err.error.message().to_owned(),
94            })?;
95        let outcome = self.update_state_after_exec(outcome);
96        Ok((self.scene_graph.clone(), outcome))
97    }
98
99    async fn new_sketch(
100        &mut self,
101        ctx: &ExecutorContext,
102        _project: ProjectId,
103        _file: FileId,
104        _version: Version,
105        args: SketchArgs,
106    ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
107        // TODO: Check version.
108
109        // Create updated KCL source from args.
110        let plane_ast = match &args.on {
111            // TODO: sketch-api: implement ObjectId to source.
112            api::Plane::Object(_) => todo!(),
113            api::Plane::Default(plane) => ast_name_expr(plane.to_string()),
114        };
115        let sketch_ast = ast::SketchBlock {
116            arguments: vec![ast::LabeledArg {
117                label: Some(ast::Identifier::new("on")),
118                arg: plane_ast,
119            }],
120            body: Default::default(),
121            non_code_meta: Default::default(),
122            digest: None,
123        };
124        let mut new_ast = self.program.ast.clone();
125        // Ensure that we allow experimental features since the sketch block
126        // won't work without it.
127        new_ast.set_experimental_features(Some(WarningLevel::Allow));
128        // Add a sketch block.
129        new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
130            inner: ast::ExpressionStatement {
131                expression: ast::Expr::SketchBlock(Box::new(ast::Node {
132                    inner: sketch_ast,
133                    start: Default::default(),
134                    end: Default::default(),
135                    module_id: Default::default(),
136                    outer_attrs: Default::default(),
137                    pre_comments: Default::default(),
138                    comment_start: Default::default(),
139                })),
140                digest: None,
141            },
142            start: Default::default(),
143            end: Default::default(),
144            module_id: Default::default(),
145            outer_attrs: Default::default(),
146            pre_comments: Default::default(),
147            comment_start: Default::default(),
148        }));
149        // Convert to string source to create real source ranges.
150        let new_source = source_from_ast(&new_ast);
151        // Parse the new source.
152        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
153        if !errors.is_empty() {
154            return Err(Error {
155                msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
156            });
157        }
158        let Some(new_program) = new_program else {
159            return Err(Error {
160                msg: "No AST produced after adding sketch".to_owned(),
161            });
162        };
163
164        let sketch_source_range = new_program
165            .ast
166            .body
167            .last()
168            .map(SourceRange::from)
169            .ok_or_else(|| Error {
170                msg: "No AST body items after adding sketch".to_owned(),
171            })?;
172        #[cfg(not(feature = "artifact-graph"))]
173        let _ = sketch_source_range;
174
175        // Make sure to only set this if there are no errors.
176        self.program = new_program.clone();
177
178        // Execute.
179        let outcome = ctx
180            .run_mock(&new_program, &MockConfig::default())
181            .await
182            .map_err(|err| {
183                // TODO: sketch-api: Yeah, this needs to change. We need to
184                // return the full error.
185                Error {
186                    msg: err.error.message().to_owned(),
187                }
188            })?;
189
190        #[cfg(not(feature = "artifact-graph"))]
191        let sketch_id = ObjectId(0);
192        #[cfg(feature = "artifact-graph")]
193        let sketch_id = outcome
194            .source_range_to_object
195            .get(&sketch_source_range)
196            .copied()
197            .ok_or_else(|| Error {
198                msg: format!("Source range of sketch not found: {sketch_source_range:?}"),
199            })?;
200        let src_delta = SourceDelta { text: new_source };
201        // Store the object in the scene.
202        self.scene_graph.sketch_mode = Some(sketch_id);
203        let outcome = self.update_state_after_exec(outcome);
204        let scene_graph_delta = SceneGraphDelta {
205            new_graph: self.scene_graph.clone(),
206            invalidates_ids: false,
207            new_objects: vec![sketch_id],
208            exec_outcome: outcome,
209        };
210        Ok((src_delta, scene_graph_delta, sketch_id))
211    }
212
213    async fn edit_sketch(
214        &mut self,
215        ctx: &ExecutorContext,
216        _project: ProjectId,
217        _file: FileId,
218        _version: Version,
219        sketch: ObjectId,
220    ) -> api::Result<SceneGraphDelta> {
221        // TODO: Check version.
222
223        // Look up existing sketch.
224        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
225            msg: format!("Sketch not found: {sketch:?}"),
226        })?;
227        let ObjectKind::Sketch(_) = &sketch_object.kind else {
228            return Err(Error {
229                msg: format!("Object is not a sketch: {sketch_object:?}"),
230            });
231        };
232
233        // Enter sketch mode by setting the sketch_mode.
234        self.scene_graph.sketch_mode = Some(sketch);
235
236        // Execute in mock mode to ensure state is up to date. The caller will
237        // want freedom analysis to display segments correctly.
238        let mock_config = MockConfig {
239            freedom_analysis: true,
240            ..Default::default()
241        };
242        let outcome = ctx.run_mock(&self.program, &mock_config).await.map_err(|err| {
243            // TODO: sketch-api: Yeah, this needs to change. We need to
244            // return the full error.
245            Error {
246                msg: err.error.message().to_owned(),
247            }
248        })?;
249
250        let outcome = self.update_state_after_exec(outcome);
251        let scene_graph_delta = SceneGraphDelta {
252            new_graph: self.scene_graph.clone(),
253            invalidates_ids: false,
254            new_objects: Vec::new(),
255            exec_outcome: outcome,
256        };
257        Ok(scene_graph_delta)
258    }
259
260    async fn exit_sketch(
261        &mut self,
262        ctx: &ExecutorContext,
263        _version: Version,
264        sketch: ObjectId,
265    ) -> api::Result<SceneGraph> {
266        // TODO: Check version.
267        #[cfg(not(target_arch = "wasm32"))]
268        let _ = sketch;
269        #[cfg(target_arch = "wasm32")]
270        if self.scene_graph.sketch_mode != Some(sketch) {
271            web_sys::console::warn_1(
272                &format!(
273                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
274                    &self.scene_graph.sketch_mode
275                )
276                .into(),
277            );
278        }
279        self.scene_graph.sketch_mode = None;
280
281        // Execute.
282        let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
283            // TODO: sketch-api: Yeah, this needs to change. We need to
284            // return the full error.
285            Error {
286                msg: err.error.message().to_owned(),
287            }
288        })?;
289
290        self.update_state_after_exec(outcome);
291
292        Ok(self.scene_graph.clone())
293    }
294
295    async fn delete_sketch(
296        &mut self,
297        ctx: &ExecutorContext,
298        _version: Version,
299        sketch: ObjectId,
300    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
301        // TODO: Check version.
302
303        let mut new_ast = self.program.ast.clone();
304
305        // Look up existing sketch.
306        let sketch_id = sketch;
307        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
308            msg: format!("Sketch not found: {sketch:?}"),
309        })?;
310        let ObjectKind::Sketch(_) = &sketch_object.kind else {
311            return Err(Error {
312                msg: format!("Object is not a sketch: {sketch_object:?}"),
313            });
314        };
315
316        // Modify the AST to remove the sketch.
317        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
318
319        self.execute_after_edit(ctx, Default::default(), true, &mut new_ast)
320            .await
321    }
322
323    async fn add_segment(
324        &mut self,
325        ctx: &ExecutorContext,
326        _version: Version,
327        sketch: ObjectId,
328        segment: SegmentCtor,
329        _label: Option<String>,
330    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
331        // TODO: Check version.
332        match segment {
333            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
334            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
335            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
336            _ => Err(Error {
337                msg: format!("segment ctor not implemented yet: {segment:?}"),
338            }),
339        }
340    }
341
342    async fn edit_segments(
343        &mut self,
344        ctx: &ExecutorContext,
345        _version: Version,
346        sketch: ObjectId,
347        segments: Vec<ExistingSegmentCtor>,
348    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
349        // TODO: Check version.
350        let mut new_ast = self.program.ast.clone();
351        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
352        for segment in segments {
353            segment_ids_edited.insert(segment.id);
354            match segment.ctor {
355                SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
356                SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
357                SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
358                _ => {
359                    return Err(Error {
360                        msg: format!("segment ctor not implemented yet: {segment:?}"),
361                    });
362                }
363            }
364        }
365        self.execute_after_edit(ctx, segment_ids_edited, false, &mut new_ast)
366            .await
367    }
368
369    async fn delete_objects(
370        &mut self,
371        ctx: &ExecutorContext,
372        _version: Version,
373        sketch: ObjectId,
374        constraint_ids: Vec<ObjectId>,
375        segment_ids: Vec<ObjectId>,
376    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
377        // TODO: Check version.
378
379        // Deduplicate IDs.
380        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
381        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
382        // Find constraints that reference the segments to be deleted, and add
383        // those to the set to be deleted.
384        self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
385
386        let mut new_ast = self.program.ast.clone();
387        for constraint_id in constraint_ids_set {
388            self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
389        }
390        for segment_id in segment_ids_set {
391            self.delete_segment(&mut new_ast, sketch, segment_id)?;
392        }
393        self.execute_after_edit(ctx, Default::default(), true, &mut new_ast)
394            .await
395    }
396
397    async fn add_constraint(
398        &mut self,
399        ctx: &ExecutorContext,
400        _version: Version,
401        sketch: ObjectId,
402        constraint: Constraint,
403    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
404        // TODO: Check version.
405
406        let mut new_ast = self.program.ast.clone();
407        let sketch_block_range = match constraint {
408            Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
409            Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
410            Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
411            Constraint::LinesEqualLength(lines_equal_length) => {
412                self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
413                    .await?
414            }
415            Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
416            Constraint::Perpendicular(perpendicular) => {
417                self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
418            }
419            Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
420        };
421        self.execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
422            .await
423    }
424
425    async fn edit_constraint(
426        &mut self,
427        _ctx: &ExecutorContext,
428        _version: Version,
429        _sketch: ObjectId,
430        _constraint_id: ObjectId,
431        _constraint: Constraint,
432    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
433        todo!()
434    }
435}
436
437impl FrontendState {
438    pub async fn hack_set_program(
439        &mut self,
440        ctx: &ExecutorContext,
441        program: Program,
442    ) -> api::Result<(SceneGraph, ExecOutcome)> {
443        self.program = program.clone();
444
445        // Execute so that the objects are updated and available for the next
446        // API call.
447        let outcome = ctx.run_with_caching(program).await.map_err(|err| {
448            // TODO: sketch-api: Yeah, this needs to change. We need to
449            // return the full error.
450            Error {
451                msg: err.error.message().to_owned(),
452            }
453        })?;
454
455        let outcome = self.update_state_after_exec(outcome);
456
457        Ok((self.scene_graph.clone(), outcome))
458    }
459
460    async fn add_point(
461        &mut self,
462        ctx: &ExecutorContext,
463        sketch: ObjectId,
464        ctor: PointCtor,
465    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
466        // Create updated KCL source from args.
467        let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
468        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
469            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
470            unlabeled: None,
471            arguments: vec![ast::LabeledArg {
472                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
473                arg: at_ast,
474            }],
475            digest: None,
476            non_code_meta: Default::default(),
477        })));
478
479        // Look up existing sketch.
480        let sketch_id = sketch;
481        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
482            #[cfg(target_arch = "wasm32")]
483            web_sys::console::error_1(
484                &format!(
485                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
486                    &self.scene_graph.objects
487                )
488                .into(),
489            );
490            Error {
491                msg: format!("Sketch not found: {sketch:?}"),
492            }
493        })?;
494        let ObjectKind::Sketch(_) = &sketch_object.kind else {
495            return Err(Error {
496                msg: format!("Object is not a sketch: {sketch_object:?}"),
497            });
498        };
499        // Add the point to the AST of the sketch block.
500        let mut new_ast = self.program.ast.clone();
501        let (sketch_block_range, _) = self.mutate_ast(
502            &mut new_ast,
503            sketch_id,
504            AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
505        )?;
506        // Convert to string source to create real source ranges.
507        let new_source = source_from_ast(&new_ast);
508        // Parse the new KCL source.
509        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
510        if !errors.is_empty() {
511            return Err(Error {
512                msg: format!("Error parsing KCL source after adding point: {errors:?}"),
513            });
514        }
515        let Some(new_program) = new_program else {
516            return Err(Error {
517                msg: "No AST produced after adding point".to_string(),
518            });
519        };
520
521        let point_source_range =
522            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
523                msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
524            })?;
525        #[cfg(not(feature = "artifact-graph"))]
526        let _ = point_source_range;
527
528        // Make sure to only set this if there are no errors.
529        self.program = new_program.clone();
530
531        // Execute.
532        let outcome = ctx
533            .run_mock(&new_program, &MockConfig::default())
534            .await
535            .map_err(|err| {
536                // TODO: sketch-api: Yeah, this needs to change. We need to
537                // return the full error.
538                Error {
539                    msg: err.error.message().to_owned(),
540                }
541            })?;
542
543        #[cfg(not(feature = "artifact-graph"))]
544        let new_object_ids = Vec::new();
545        #[cfg(feature = "artifact-graph")]
546        let new_object_ids = {
547            let segment_id = outcome
548                .source_range_to_object
549                .get(&point_source_range)
550                .copied()
551                .ok_or_else(|| Error {
552                    msg: format!("Source range of point not found: {point_source_range:?}"),
553                })?;
554            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
555                msg: format!("Segment not found: {segment_id:?}"),
556            })?;
557            let ObjectKind::Segment { segment } = &segment_object.kind else {
558                return Err(Error {
559                    msg: format!("Object is not a segment: {segment_object:?}"),
560                });
561            };
562            let Segment::Point(_) = segment else {
563                return Err(Error {
564                    msg: format!("Segment is not a point: {segment:?}"),
565                });
566            };
567            vec![segment_id]
568        };
569        let src_delta = SourceDelta { text: new_source };
570        let outcome = self.update_state_after_exec(outcome);
571        let scene_graph_delta = SceneGraphDelta {
572            new_graph: self.scene_graph.clone(),
573            invalidates_ids: false,
574            new_objects: new_object_ids,
575            exec_outcome: outcome,
576        };
577        Ok((src_delta, scene_graph_delta))
578    }
579
580    async fn add_line(
581        &mut self,
582        ctx: &ExecutorContext,
583        sketch: ObjectId,
584        ctor: LineCtor,
585    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
586        // Create updated KCL source from args.
587        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
588        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
589        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
590            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
591            unlabeled: None,
592            arguments: vec![
593                ast::LabeledArg {
594                    label: Some(ast::Identifier::new(LINE_START_PARAM)),
595                    arg: start_ast,
596                },
597                ast::LabeledArg {
598                    label: Some(ast::Identifier::new(LINE_END_PARAM)),
599                    arg: end_ast,
600                },
601            ],
602            digest: None,
603            non_code_meta: Default::default(),
604        })));
605
606        // Look up existing sketch.
607        let sketch_id = sketch;
608        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
609            msg: format!("Sketch not found: {sketch:?}"),
610        })?;
611        let ObjectKind::Sketch(_) = &sketch_object.kind else {
612            return Err(Error {
613                msg: format!("Object is not a sketch: {sketch_object:?}"),
614            });
615        };
616        // Add the line to the AST of the sketch block.
617        let mut new_ast = self.program.ast.clone();
618        let (sketch_block_range, _) = self.mutate_ast(
619            &mut new_ast,
620            sketch_id,
621            AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
622        )?;
623        // Convert to string source to create real source ranges.
624        let new_source = source_from_ast(&new_ast);
625        // Parse the new KCL source.
626        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
627        if !errors.is_empty() {
628            return Err(Error {
629                msg: format!("Error parsing KCL source after adding line: {errors:?}"),
630            });
631        }
632        let Some(new_program) = new_program else {
633            return Err(Error {
634                msg: "No AST produced after adding line".to_string(),
635            });
636        };
637        let line_source_range =
638            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
639                msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
640            })?;
641        #[cfg(not(feature = "artifact-graph"))]
642        let _ = line_source_range;
643
644        // Make sure to only set this if there are no errors.
645        self.program = new_program.clone();
646
647        // Execute.
648        let outcome = ctx
649            .run_mock(&new_program, &MockConfig::default())
650            .await
651            .map_err(|err| {
652                // TODO: sketch-api: Yeah, this needs to change. We need to
653                // return the full error.
654                Error {
655                    msg: err.error.message().to_owned(),
656                }
657            })?;
658
659        #[cfg(not(feature = "artifact-graph"))]
660        let new_object_ids = Vec::new();
661        #[cfg(feature = "artifact-graph")]
662        let new_object_ids = {
663            let segment_id = outcome
664                .source_range_to_object
665                .get(&line_source_range)
666                .copied()
667                .ok_or_else(|| Error {
668                    msg: format!("Source range of line not found: {line_source_range:?}"),
669                })?;
670            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
671                msg: format!("Segment not found: {segment_id:?}"),
672            })?;
673            let ObjectKind::Segment { segment } = &segment_object.kind else {
674                return Err(Error {
675                    msg: format!("Object is not a segment: {segment_object:?}"),
676                });
677            };
678            let Segment::Line(line) = segment else {
679                return Err(Error {
680                    msg: format!("Segment is not a line: {segment:?}"),
681                });
682            };
683            vec![line.start, line.end, segment_id]
684        };
685        let src_delta = SourceDelta { text: new_source };
686        let outcome = self.update_state_after_exec(outcome);
687        let scene_graph_delta = SceneGraphDelta {
688            new_graph: self.scene_graph.clone(),
689            invalidates_ids: false,
690            new_objects: new_object_ids,
691            exec_outcome: outcome,
692        };
693        Ok((src_delta, scene_graph_delta))
694    }
695
696    async fn add_arc(
697        &mut self,
698        ctx: &ExecutorContext,
699        sketch: ObjectId,
700        ctor: ArcCtor,
701    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
702        // Create updated KCL source from args.
703        let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
704        let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
705        let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
706        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
707            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
708            unlabeled: None,
709            arguments: vec![
710                ast::LabeledArg {
711                    label: Some(ast::Identifier::new(ARC_START_PARAM)),
712                    arg: start_ast,
713                },
714                ast::LabeledArg {
715                    label: Some(ast::Identifier::new(ARC_END_PARAM)),
716                    arg: end_ast,
717                },
718                ast::LabeledArg {
719                    label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
720                    arg: center_ast,
721                },
722            ],
723            digest: None,
724            non_code_meta: Default::default(),
725        })));
726
727        // Look up existing sketch.
728        let sketch_id = sketch;
729        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
730            msg: format!("Sketch not found: {sketch:?}"),
731        })?;
732        let ObjectKind::Sketch(_) = &sketch_object.kind else {
733            return Err(Error {
734                msg: format!("Object is not a sketch: {sketch_object:?}"),
735            });
736        };
737        // Add the arc to the AST of the sketch block.
738        let mut new_ast = self.program.ast.clone();
739        let (sketch_block_range, _) = self.mutate_ast(
740            &mut new_ast,
741            sketch_id,
742            AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
743        )?;
744        // Convert to string source to create real source ranges.
745        let new_source = source_from_ast(&new_ast);
746        // Parse the new KCL source.
747        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
748        if !errors.is_empty() {
749            return Err(Error {
750                msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
751            });
752        }
753        let Some(new_program) = new_program else {
754            return Err(Error {
755                msg: "No AST produced after adding arc".to_string(),
756            });
757        };
758        let arc_source_range =
759            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
760                msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
761            })?;
762        #[cfg(not(feature = "artifact-graph"))]
763        let _ = arc_source_range;
764
765        // Make sure to only set this if there are no errors.
766        self.program = new_program.clone();
767
768        // Execute.
769        let outcome = ctx
770            .run_mock(&new_program, &MockConfig::default())
771            .await
772            .map_err(|err| {
773                // TODO: sketch-api: Yeah, this needs to change. We need to
774                // return the full error.
775                Error {
776                    msg: err.error.message().to_owned(),
777                }
778            })?;
779
780        #[cfg(not(feature = "artifact-graph"))]
781        let new_object_ids = Vec::new();
782        #[cfg(feature = "artifact-graph")]
783        let new_object_ids = {
784            let segment_id = outcome
785                .source_range_to_object
786                .get(&arc_source_range)
787                .copied()
788                .ok_or_else(|| Error {
789                    msg: format!("Source range of arc not found: {arc_source_range:?}"),
790                })?;
791            let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
792                msg: format!("Segment not found: {segment_id:?}"),
793            })?;
794            let ObjectKind::Segment { segment } = &segment_object.kind else {
795                return Err(Error {
796                    msg: format!("Object is not a segment: {segment_object:?}"),
797                });
798            };
799            let Segment::Arc(arc) = segment else {
800                return Err(Error {
801                    msg: format!("Segment is not an arc: {segment:?}"),
802                });
803            };
804            vec![arc.start, arc.end, arc.center, segment_id]
805        };
806        let src_delta = SourceDelta { text: new_source };
807        let outcome = self.update_state_after_exec(outcome);
808        let scene_graph_delta = SceneGraphDelta {
809            new_graph: self.scene_graph.clone(),
810            invalidates_ids: false,
811            new_objects: new_object_ids,
812            exec_outcome: outcome,
813        };
814        Ok((src_delta, scene_graph_delta))
815    }
816
817    fn edit_point(
818        &mut self,
819        new_ast: &mut ast::Node<ast::Program>,
820        sketch: ObjectId,
821        point: ObjectId,
822        ctor: PointCtor,
823    ) -> api::Result<()> {
824        // Create updated KCL source from args.
825        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
826
827        // Look up existing sketch.
828        let sketch_id = sketch;
829        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
830            msg: format!("Sketch not found: {sketch:?}"),
831        })?;
832        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
833            return Err(Error {
834                msg: format!("Object is not a sketch: {sketch_object:?}"),
835            });
836        };
837        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
838            msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
839        })?;
840        // Look up existing point.
841        let point_id = point;
842        let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
843            msg: format!("Point not found in scene graph: point={point:?}"),
844        })?;
845        let ObjectKind::Segment {
846            segment: Segment::Point(point),
847        } = &point_object.kind
848        else {
849            return Err(Error {
850                msg: format!("Object is not a point segment: {point_object:?}"),
851            });
852        };
853
854        // If the point is part of a line, edit the line instead.
855        if let Some(line_id) = point.owner {
856            let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
857                msg: format!("Internal: Line owner of point not found in scene graph: line={line_id:?}",),
858            })?;
859            let ObjectKind::Segment {
860                segment: Segment::Line(line),
861            } = &line_object.kind
862            else {
863                return Err(Error {
864                    msg: format!("Internal: Owner of point is not actually a line segment: {line_object:?}"),
865                });
866            };
867            let SegmentCtor::Line(line_ctor) = &line.ctor else {
868                return Err(Error {
869                    msg: format!("Internal: Owner of point does not have line ctor: {line_object:?}"),
870                });
871            };
872            let mut line_ctor = line_ctor.clone();
873            // Which end of the line is this point?
874            if line.start == point_id {
875                line_ctor.start = ctor.position;
876            } else if line.end == point_id {
877                line_ctor.end = ctor.position;
878            } else {
879                return Err(Error {
880                    msg: format!(
881                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={line_id:?}"
882                    ),
883                });
884            }
885            return self.edit_line(new_ast, sketch_id, line_id, line_ctor);
886        }
887
888        // Modify the point AST.
889        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
890        Ok(())
891    }
892
893    fn edit_line(
894        &mut self,
895        new_ast: &mut ast::Node<ast::Program>,
896        sketch: ObjectId,
897        line: ObjectId,
898        ctor: LineCtor,
899    ) -> api::Result<()> {
900        // Create updated KCL source from args.
901        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
902        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
903
904        // Look up existing sketch.
905        let sketch_id = sketch;
906        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
907            msg: format!("Sketch not found: {sketch:?}"),
908        })?;
909        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
910            return Err(Error {
911                msg: format!("Object is not a sketch: {sketch_object:?}"),
912            });
913        };
914        sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
915            msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
916        })?;
917        // Look up existing line.
918        let line_id = line;
919        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
920            msg: format!("Line not found in scene graph: line={line:?}"),
921        })?;
922        let ObjectKind::Segment { .. } = &line_object.kind else {
923            return Err(Error {
924                msg: format!("Object is not a segment: {line_object:?}"),
925            });
926        };
927
928        // Modify the line AST.
929        self.mutate_ast(
930            new_ast,
931            line_id,
932            AstMutateCommand::EditLine {
933                start: new_start_ast,
934                end: new_end_ast,
935            },
936        )?;
937        Ok(())
938    }
939
940    fn edit_arc(
941        &mut self,
942        new_ast: &mut ast::Node<ast::Program>,
943        sketch: ObjectId,
944        arc: ObjectId,
945        ctor: ArcCtor,
946    ) -> api::Result<()> {
947        // Create updated KCL source from args.
948        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
949        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
950        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
951
952        // Look up existing sketch.
953        let sketch_id = sketch;
954        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
955            msg: format!("Sketch not found: {sketch:?}"),
956        })?;
957        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
958            return Err(Error {
959                msg: format!("Object is not a sketch: {sketch_object:?}"),
960            });
961        };
962        sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
963            msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
964        })?;
965        // Look up existing arc.
966        let arc_id = arc;
967        let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
968            msg: format!("Arc not found in scene graph: arc={arc:?}"),
969        })?;
970        let ObjectKind::Segment { .. } = &arc_object.kind else {
971            return Err(Error {
972                msg: format!("Object is not a segment: {arc_object:?}"),
973            });
974        };
975
976        // Modify the arc AST.
977        self.mutate_ast(
978            new_ast,
979            arc_id,
980            AstMutateCommand::EditArc {
981                start: new_start_ast,
982                end: new_end_ast,
983                center: new_center_ast,
984            },
985        )?;
986        Ok(())
987    }
988
989    fn delete_segment(
990        &mut self,
991        new_ast: &mut ast::Node<ast::Program>,
992        sketch: ObjectId,
993        segment_id: ObjectId,
994    ) -> api::Result<()> {
995        // Look up existing sketch.
996        let sketch_id = sketch;
997        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
998            msg: format!("Sketch not found: {sketch:?}"),
999        })?;
1000        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1001            return Err(Error {
1002                msg: format!("Object is not a sketch: {sketch_object:?}"),
1003            });
1004        };
1005        sketch
1006            .segments
1007            .iter()
1008            .find(|o| **o == segment_id)
1009            .ok_or_else(|| Error {
1010                msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1011            })?;
1012        // Look up existing segment.
1013        let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1014            msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1015        })?;
1016        let ObjectKind::Segment { .. } = &segment_object.kind else {
1017            return Err(Error {
1018                msg: format!("Object is not a segment: {segment_object:?}"),
1019            });
1020        };
1021
1022        // Modify the AST to remove the segment.
1023        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1024        Ok(())
1025    }
1026
1027    fn delete_constraint(
1028        &mut self,
1029        new_ast: &mut ast::Node<ast::Program>,
1030        sketch: ObjectId,
1031        constraint_id: ObjectId,
1032    ) -> api::Result<()> {
1033        // Look up existing sketch.
1034        let sketch_id = sketch;
1035        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1036            msg: format!("Sketch not found: {sketch:?}"),
1037        })?;
1038        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1039            return Err(Error {
1040                msg: format!("Object is not a sketch: {sketch_object:?}"),
1041            });
1042        };
1043        sketch
1044            .constraints
1045            .iter()
1046            .find(|o| **o == constraint_id)
1047            .ok_or_else(|| Error {
1048                msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1049            })?;
1050        // Look up existing constraint.
1051        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1052            msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1053        })?;
1054        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1055            return Err(Error {
1056                msg: format!("Object is not a constraint: {constraint_object:?}"),
1057            });
1058        };
1059
1060        // Modify the AST to remove the constraint.
1061        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1062        Ok(())
1063    }
1064
1065    async fn execute_after_edit(
1066        &mut self,
1067        ctx: &ExecutorContext,
1068        segment_ids_edited: AhashIndexSet<ObjectId>,
1069        is_delete: bool,
1070        new_ast: &mut ast::Node<ast::Program>,
1071    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1072        // Convert to string source to create real source ranges.
1073        let new_source = source_from_ast(new_ast);
1074        // Parse the new KCL source.
1075        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1076        if !errors.is_empty() {
1077            return Err(Error {
1078                msg: format!("Error parsing KCL source after editing: {errors:?}"),
1079            });
1080        }
1081        let Some(new_program) = new_program else {
1082            return Err(Error {
1083                msg: "No AST produced after editing".to_string(),
1084            });
1085        };
1086
1087        // TODO: sketch-api: make sure to only set this if there are no errors.
1088        self.program = new_program.clone();
1089
1090        #[cfg(not(feature = "artifact-graph"))]
1091        drop(segment_ids_edited);
1092
1093        // Execute.
1094        let mock_config = MockConfig {
1095            use_prev_memory: !is_delete,
1096            freedom_analysis: is_delete,
1097            #[cfg(feature = "artifact-graph")]
1098            segment_ids_edited,
1099        };
1100        let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1101            // TODO: sketch-api: Yeah, this needs to change. We need to
1102            // return the full error.
1103            Error {
1104                msg: err.error.message().to_owned(),
1105            }
1106        })?;
1107
1108        let outcome = self.update_state_after_exec(outcome);
1109
1110        #[cfg(feature = "artifact-graph")]
1111        let new_source = {
1112            // Feed back sketch var solutions into the source.
1113            //
1114            // TODO: Limit to only the sketch ID parameter. Currently, the
1115            // interpreter is returning all var solutions from the last sketch
1116            // block.
1117            let mut new_ast = self.program.ast.clone();
1118            for (var_range, value) in &outcome.var_solutions {
1119                let rounded = value.round(3);
1120                mutate_ast_node_by_source_range(
1121                    &mut new_ast,
1122                    *var_range,
1123                    AstMutateCommand::EditVarInitialValue { value: rounded },
1124                )?;
1125            }
1126            source_from_ast(&new_ast)
1127        };
1128
1129        let src_delta = SourceDelta { text: new_source };
1130        let scene_graph_delta = SceneGraphDelta {
1131            new_graph: self.scene_graph.clone(),
1132            invalidates_ids: is_delete,
1133            new_objects: Vec::new(),
1134            exec_outcome: outcome,
1135        };
1136        Ok((src_delta, scene_graph_delta))
1137    }
1138
1139    async fn add_coincident(
1140        &mut self,
1141        sketch: ObjectId,
1142        coincident: Coincident,
1143        new_ast: &mut ast::Node<ast::Program>,
1144    ) -> api::Result<SourceRange> {
1145        let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1146            return Err(Error {
1147                msg: format!(
1148                    "Coincident constraint must have exactly 2 segments, got {}",
1149                    coincident.segments.len()
1150                ),
1151            });
1152        };
1153        let sketch_id = sketch;
1154
1155        // Get AST reference for first object (point or segment)
1156        let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1157            msg: format!("Object not found: {seg0_id:?}"),
1158        })?;
1159        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1160            return Err(Error {
1161                msg: format!("Object is not a segment: {seg0_object:?}"),
1162            });
1163        };
1164        let seg0_ast = match seg0_segment {
1165            Segment::Point(point) => {
1166                // If the point is part of a line, refer to the line's start/end property
1167                if let Some(line_id) = point.owner {
1168                    let line = self.expect_line(line_id)?;
1169                    let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1170                    let property = if line.start == seg0_id {
1171                        LINE_PROPERTY_START
1172                    } else if line.end == seg0_id {
1173                        LINE_PROPERTY_END
1174                    } else {
1175                        return Err(Error {
1176                            msg: format!(
1177                                "Internal: Point is not part of owner's line segment: point={seg0_id:?}, line={line_id:?}"
1178                            ),
1179                        });
1180                    };
1181                    get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1182                } else {
1183                    // Standalone point
1184                    get_or_insert_ast_reference(new_ast, &seg0_object.source, "point", None)?
1185                }
1186            }
1187            Segment::Line(_) => {
1188                // Reference the segment directly (for point-segment coincident)
1189                get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1190            }
1191            Segment::Arc(_) | Segment::Circle(_) => {
1192                return Err(Error {
1193                    msg: "Coincident constraint with arcs or circles is not supported. Only points and line segments are.".to_owned(),
1194                });
1195            }
1196        };
1197
1198        // Get AST reference for second object (point or segment)
1199        let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1200            msg: format!("Object not found: {seg1_id:?}"),
1201        })?;
1202        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1203            return Err(Error {
1204                msg: format!("Object is not a segment: {seg1_object:?}"),
1205            });
1206        };
1207        let seg1_ast = match seg1_segment {
1208            Segment::Point(point) => {
1209                // If the point is part of a line, refer to the line's start/end property
1210                if let Some(line_id) = point.owner {
1211                    let line = self.expect_line(line_id)?;
1212                    let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1213                    let property = if line.start == seg1_id {
1214                        LINE_PROPERTY_START
1215                    } else if line.end == seg1_id {
1216                        LINE_PROPERTY_END
1217                    } else {
1218                        return Err(Error {
1219                            msg: format!(
1220                                "Internal: Point is not part of owner's line segment: point={seg1_id:?}, line={line_id:?}"
1221                            ),
1222                        });
1223                    };
1224                    get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1225                } else {
1226                    // Standalone point
1227                    get_or_insert_ast_reference(new_ast, &seg1_object.source, "point", None)?
1228                }
1229            }
1230            Segment::Line(_) => {
1231                // Reference the segment directly (for point-segment coincident)
1232                get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1233            }
1234            Segment::Arc(_) | Segment::Circle(_) => {
1235                return Err(Error {
1236                    msg: "Coincident constraint with arcs or circles is not supported. Only points and line segments are.".to_owned(),
1237                });
1238            }
1239        };
1240
1241        // Create the coincident() call.
1242        let coincident_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1243            callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
1244            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1245                ast::ArrayExpression {
1246                    elements: vec![seg0_ast, seg1_ast],
1247                    digest: None,
1248                    non_code_meta: Default::default(),
1249                },
1250            )))),
1251            arguments: Default::default(),
1252            digest: None,
1253            non_code_meta: Default::default(),
1254        })));
1255
1256        // Add the line to the AST of the sketch block.
1257        let (sketch_block_range, _) = self.mutate_ast(
1258            new_ast,
1259            sketch_id,
1260            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1261        )?;
1262        Ok(sketch_block_range)
1263    }
1264
1265    async fn add_distance(
1266        &mut self,
1267        sketch: ObjectId,
1268        distance: Distance,
1269        new_ast: &mut ast::Node<ast::Program>,
1270    ) -> api::Result<SourceRange> {
1271        let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1272            return Err(Error {
1273                msg: format!(
1274                    "Distance constraint must have exactly 2 points, got {}",
1275                    distance.points.len()
1276                ),
1277            });
1278        };
1279        let sketch_id = sketch;
1280
1281        // Map the runtime objects back to variable names.
1282        let pt0_object = self.scene_graph.objects.get(pt0_id.0).ok_or_else(|| Error {
1283            msg: format!("Point not found: {pt0_id:?}"),
1284        })?;
1285        let ObjectKind::Segment { segment: pt0_segment } = &pt0_object.kind else {
1286            return Err(Error {
1287                msg: format!("Object is not a segment: {pt0_object:?}"),
1288            });
1289        };
1290        let Segment::Point(pt0) = pt0_segment else {
1291            return Err(Error {
1292                msg: format!("Only points are currently supported: {pt0_object:?}"),
1293            });
1294        };
1295        // If the point is part of a line, refer to the line instead.
1296        let pt0_ast = if let Some(line_id) = pt0.owner {
1297            let line = self.expect_line(line_id)?;
1298            let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1299            let property = if line.start == pt0_id {
1300                LINE_PROPERTY_START
1301            } else if line.end == pt0_id {
1302                LINE_PROPERTY_END
1303            } else {
1304                return Err(Error {
1305                    msg: format!(
1306                        "Internal: Point is not part of owner's line segment: point={pt0_id:?}, line={line_id:?}"
1307                    ),
1308                });
1309            };
1310            get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1311        } else {
1312            get_or_insert_ast_reference(new_ast, &pt0_object.source, "point", None)?
1313        };
1314
1315        let pt1_object = self.scene_graph.objects.get(pt1_id.0).ok_or_else(|| Error {
1316            msg: format!("Point not found: {pt1_id:?}"),
1317        })?;
1318        let ObjectKind::Segment { segment: pt1_segment } = &pt1_object.kind else {
1319            return Err(Error {
1320                msg: format!("Object is not a segment: {pt1_object:?}"),
1321            });
1322        };
1323        let Segment::Point(pt1) = pt1_segment else {
1324            return Err(Error {
1325                msg: format!("Only points are currently supported: {pt1_object:?}"),
1326            });
1327        };
1328        // If the point is part of a line, refer to the line instead.
1329        let pt1_ast = if let Some(line_id) = pt1.owner {
1330            let line = self.expect_line(line_id)?;
1331            let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1332            let property = if line.start == pt1_id {
1333                LINE_PROPERTY_START
1334            } else if line.end == pt1_id {
1335                LINE_PROPERTY_END
1336            } else {
1337                return Err(Error {
1338                    msg: format!(
1339                        "Internal: Point is not part of owner's line segment: point={pt1_id:?}, line={line_id:?}"
1340                    ),
1341                });
1342            };
1343            get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1344        } else {
1345            get_or_insert_ast_reference(new_ast, &pt1_object.source, "point", None)?
1346        };
1347
1348        // Create the distance() call.
1349        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1350            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1351            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1352                ast::ArrayExpression {
1353                    elements: vec![pt0_ast, pt1_ast],
1354                    digest: None,
1355                    non_code_meta: Default::default(),
1356                },
1357            )))),
1358            arguments: Default::default(),
1359            digest: None,
1360            non_code_meta: Default::default(),
1361        })));
1362        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1363            left: distance_call_ast,
1364            operator: ast::BinaryOperator::Eq,
1365            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1366                value: ast::LiteralValue::Number {
1367                    value: distance.distance.value,
1368                    suffix: distance.distance.units,
1369                },
1370                raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1371                    msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1372                })?,
1373                digest: None,
1374            }))),
1375            digest: None,
1376        })));
1377
1378        // Add the line to the AST of the sketch block.
1379        let (sketch_block_range, _) = self.mutate_ast(
1380            new_ast,
1381            sketch_id,
1382            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1383        )?;
1384        Ok(sketch_block_range)
1385    }
1386
1387    async fn add_horizontal(
1388        &mut self,
1389        sketch: ObjectId,
1390        horizontal: Horizontal,
1391        new_ast: &mut ast::Node<ast::Program>,
1392    ) -> api::Result<SourceRange> {
1393        let sketch_id = sketch;
1394
1395        // Map the runtime objects back to variable names.
1396        let line_id = horizontal.line;
1397        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1398            msg: format!("Line not found: {line_id:?}"),
1399        })?;
1400        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1401            return Err(Error {
1402                msg: format!("Object is not a segment: {line_object:?}"),
1403            });
1404        };
1405        let Segment::Line(_) = line_segment else {
1406            return Err(Error {
1407                msg: format!("Only lines can be made horizontal: {line_object:?}"),
1408            });
1409        };
1410        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1411
1412        // Create the horizontal() call.
1413        let horizontal_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1414            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
1415            unlabeled: Some(line_ast),
1416            arguments: Default::default(),
1417            digest: None,
1418            non_code_meta: Default::default(),
1419        })));
1420
1421        // Add the line to the AST of the sketch block.
1422        let (sketch_block_range, _) = self.mutate_ast(
1423            new_ast,
1424            sketch_id,
1425            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
1426        )?;
1427        Ok(sketch_block_range)
1428    }
1429
1430    async fn add_lines_equal_length(
1431        &mut self,
1432        sketch: ObjectId,
1433        lines_equal_length: LinesEqualLength,
1434        new_ast: &mut ast::Node<ast::Program>,
1435    ) -> api::Result<SourceRange> {
1436        let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
1437            return Err(Error {
1438                msg: format!(
1439                    "Lines equal length constraint must have exactly 2 lines, got {}",
1440                    lines_equal_length.lines.len()
1441                ),
1442            });
1443        };
1444
1445        let sketch_id = sketch;
1446
1447        // Map the runtime objects back to variable names.
1448        let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1449            msg: format!("Line not found: {line0_id:?}"),
1450        })?;
1451        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1452            return Err(Error {
1453                msg: format!("Object is not a segment: {line0_object:?}"),
1454            });
1455        };
1456        let Segment::Line(_) = line0_segment else {
1457            return Err(Error {
1458                msg: format!("Only lines can be made equal length: {line0_object:?}"),
1459            });
1460        };
1461        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1462
1463        let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1464            msg: format!("Line not found: {line1_id:?}"),
1465        })?;
1466        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1467            return Err(Error {
1468                msg: format!("Object is not a segment: {line1_object:?}"),
1469            });
1470        };
1471        let Segment::Line(_) = line1_segment else {
1472            return Err(Error {
1473                msg: format!("Only lines can be made equal length: {line1_object:?}"),
1474            });
1475        };
1476        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1477
1478        // Create the equalLength() call.
1479        let equal_length_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1480            callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
1481            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1482                ast::ArrayExpression {
1483                    elements: vec![line0_ast, line1_ast],
1484                    digest: None,
1485                    non_code_meta: Default::default(),
1486                },
1487            )))),
1488            arguments: Default::default(),
1489            digest: None,
1490            non_code_meta: Default::default(),
1491        })));
1492
1493        // Add the constraint to the AST of the sketch block.
1494        let (sketch_block_range, _) = self.mutate_ast(
1495            new_ast,
1496            sketch_id,
1497            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
1498        )?;
1499        Ok(sketch_block_range)
1500    }
1501
1502    async fn add_parallel(
1503        &mut self,
1504        sketch: ObjectId,
1505        parallel: Parallel,
1506        new_ast: &mut ast::Node<ast::Program>,
1507    ) -> api::Result<SourceRange> {
1508        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
1509            .await
1510    }
1511
1512    async fn add_perpendicular(
1513        &mut self,
1514        sketch: ObjectId,
1515        perpendicular: Perpendicular,
1516        new_ast: &mut ast::Node<ast::Program>,
1517    ) -> api::Result<SourceRange> {
1518        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
1519            .await
1520    }
1521
1522    async fn add_lines_at_angle_constraint(
1523        &mut self,
1524        sketch: ObjectId,
1525        angle_kind: LinesAtAngleKind,
1526        lines: Vec<ObjectId>,
1527        new_ast: &mut ast::Node<ast::Program>,
1528    ) -> api::Result<SourceRange> {
1529        let &[line0_id, line1_id] = lines.as_slice() else {
1530            return Err(Error {
1531                msg: format!(
1532                    "{} constraint must have exactly 2 lines, got {}",
1533                    angle_kind.to_function_name(),
1534                    lines.len()
1535                ),
1536            });
1537        };
1538
1539        let sketch_id = sketch;
1540
1541        // Map the runtime objects back to variable names.
1542        let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1543            msg: format!("Line not found: {line0_id:?}"),
1544        })?;
1545        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1546            return Err(Error {
1547                msg: format!("Object is not a segment: {line0_object:?}"),
1548            });
1549        };
1550        let Segment::Line(_) = line0_segment else {
1551            return Err(Error {
1552                msg: format!(
1553                    "Only lines can be made {}: {line0_object:?}",
1554                    angle_kind.to_function_name()
1555                ),
1556            });
1557        };
1558        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1559
1560        let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1561            msg: format!("Line not found: {line1_id:?}"),
1562        })?;
1563        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1564            return Err(Error {
1565                msg: format!("Object is not a segment: {line1_object:?}"),
1566            });
1567        };
1568        let Segment::Line(_) = line1_segment else {
1569            return Err(Error {
1570                msg: format!(
1571                    "Only lines can be made {}: {line1_object:?}",
1572                    angle_kind.to_function_name()
1573                ),
1574            });
1575        };
1576        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1577
1578        // Create the parallel() or perpendicular() call.
1579        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1580            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
1581            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1582                ast::ArrayExpression {
1583                    elements: vec![line0_ast, line1_ast],
1584                    digest: None,
1585                    non_code_meta: Default::default(),
1586                },
1587            )))),
1588            arguments: Default::default(),
1589            digest: None,
1590            non_code_meta: Default::default(),
1591        })));
1592
1593        // Add the constraint to the AST of the sketch block.
1594        let (sketch_block_range, _) = self.mutate_ast(
1595            new_ast,
1596            sketch_id,
1597            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
1598        )?;
1599        Ok(sketch_block_range)
1600    }
1601
1602    async fn add_vertical(
1603        &mut self,
1604        sketch: ObjectId,
1605        vertical: Vertical,
1606        new_ast: &mut ast::Node<ast::Program>,
1607    ) -> api::Result<SourceRange> {
1608        let sketch_id = sketch;
1609
1610        // Map the runtime objects back to variable names.
1611        let line_id = vertical.line;
1612        let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1613            msg: format!("Line not found: {line_id:?}"),
1614        })?;
1615        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1616            return Err(Error {
1617                msg: format!("Object is not a segment: {line_object:?}"),
1618            });
1619        };
1620        let Segment::Line(_) = line_segment else {
1621            return Err(Error {
1622                msg: format!("Only lines can be made vertical: {line_object:?}"),
1623            });
1624        };
1625        let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1626
1627        // Create the vertical() call.
1628        let vertical_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1629            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
1630            unlabeled: Some(line_ast),
1631            arguments: Default::default(),
1632            digest: None,
1633            non_code_meta: Default::default(),
1634        })));
1635
1636        // Add the line to the AST of the sketch block.
1637        let (sketch_block_range, _) = self.mutate_ast(
1638            new_ast,
1639            sketch_id,
1640            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
1641        )?;
1642        Ok(sketch_block_range)
1643    }
1644
1645    async fn execute_after_add_constraint(
1646        &mut self,
1647        ctx: &ExecutorContext,
1648        _sketch_id: ObjectId,
1649        sketch_block_range: SourceRange,
1650        new_ast: &mut ast::Node<ast::Program>,
1651    ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1652        // Convert to string source to create real source ranges.
1653        let new_source = source_from_ast(new_ast);
1654        // Parse the new KCL source.
1655        let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1656        if !errors.is_empty() {
1657            return Err(Error {
1658                msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
1659            });
1660        }
1661        let Some(new_program) = new_program else {
1662            return Err(Error {
1663                msg: "No AST produced after adding constraint".to_string(),
1664            });
1665        };
1666        let _constraint_source_range =
1667            find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1668                msg: format!(
1669                    "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
1670                ),
1671            })?;
1672
1673        // Make sure to only set this if there are no errors.
1674        self.program = new_program.clone();
1675
1676        // Execute.
1677        let mock_config = MockConfig {
1678            freedom_analysis: true,
1679            ..Default::default()
1680        };
1681        let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1682            // TODO: sketch-api: Yeah, this needs to change. We need to
1683            // return the full error.
1684            Error {
1685                msg: err.error.message().to_owned(),
1686            }
1687        })?;
1688
1689        let src_delta = SourceDelta { text: new_source };
1690        let outcome = self.update_state_after_exec(outcome);
1691        let scene_graph_delta = SceneGraphDelta {
1692            new_graph: self.scene_graph.clone(),
1693            invalidates_ids: false,
1694            new_objects: Vec::new(),
1695            exec_outcome: outcome,
1696        };
1697        Ok((src_delta, scene_graph_delta))
1698    }
1699
1700    // Find constraints that reference the given segments to be deleted, and add
1701    // those to the constraint set to be deleted for cascading delete.
1702    fn add_dependent_constraints_to_delete(
1703        &self,
1704        sketch_id: ObjectId,
1705        segment_ids_set: &AhashIndexSet<ObjectId>,
1706        constraint_ids_set: &mut AhashIndexSet<ObjectId>,
1707    ) -> api::Result<()> {
1708        // Look up the sketch.
1709        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1710            msg: format!("Sketch not found: {sketch_id:?}"),
1711        })?;
1712        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1713            return Err(Error {
1714                msg: format!("Object is not a sketch: {sketch_object:?}"),
1715            });
1716        };
1717        for constraint_id in &sketch.constraints {
1718            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1719                msg: format!("Constraint not found: {constraint_id:?}"),
1720            })?;
1721            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1722                return Err(Error {
1723                    msg: format!("Object is not a constraint: {constraint_object:?}"),
1724                });
1725            };
1726            let depends_on_segment = match constraint {
1727                Constraint::Coincident(c) => c.segments.iter().any(|pt_id| {
1728                    if segment_ids_set.contains(pt_id) {
1729                        return true;
1730                    }
1731                    let pt_object = self.scene_graph.objects.get(pt_id.0);
1732                    if let Some(obj) = pt_object
1733                        && let ObjectKind::Segment { segment } = &obj.kind
1734                        && let Segment::Point(pt) = segment
1735                        && let Some(owner_line_id) = pt.owner
1736                    {
1737                        return segment_ids_set.contains(&owner_line_id);
1738                    }
1739                    false
1740                }),
1741                Constraint::Distance(d) => d.points.iter().any(|pt_id| {
1742                    let pt_object = self.scene_graph.objects.get(pt_id.0);
1743                    if let Some(obj) = pt_object
1744                        && let ObjectKind::Segment { segment } = &obj.kind
1745                        && let Segment::Point(pt) = segment
1746                        && let Some(owner_line_id) = pt.owner
1747                    {
1748                        return segment_ids_set.contains(&owner_line_id);
1749                    }
1750                    false
1751                }),
1752                Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
1753                Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
1754                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
1755                    .lines
1756                    .iter()
1757                    .any(|line_id| segment_ids_set.contains(line_id)),
1758                Constraint::Parallel(parallel) => {
1759                    parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
1760                }
1761                Constraint::Perpendicular(perpendicular) => perpendicular
1762                    .lines
1763                    .iter()
1764                    .any(|line_id| segment_ids_set.contains(line_id)),
1765            };
1766            if depends_on_segment {
1767                constraint_ids_set.insert(*constraint_id);
1768            }
1769        }
1770        Ok(())
1771    }
1772
1773    fn expect_line(&self, object_id: ObjectId) -> api::Result<&Line> {
1774        let object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1775            msg: format!("Object not found: {object_id:?}"),
1776        })?;
1777        let ObjectKind::Segment { segment } = &object.kind else {
1778            return Err(Error {
1779                msg: format!("Object is not a segment: {object:?}"),
1780            });
1781        };
1782        let Segment::Line(line) = segment else {
1783            return Err(Error {
1784                msg: format!("Segment is not a line: {segment:?}"),
1785            });
1786        };
1787        Ok(line)
1788    }
1789
1790    fn update_state_after_exec(&mut self, outcome: ExecOutcome) -> ExecOutcome {
1791        #[cfg(not(feature = "artifact-graph"))]
1792        return outcome;
1793        #[cfg(feature = "artifact-graph")]
1794        {
1795            let mut outcome = outcome;
1796            self.scene_graph.objects = std::mem::take(&mut outcome.scene_objects);
1797            outcome
1798        }
1799    }
1800
1801    fn mutate_ast(
1802        &mut self,
1803        ast: &mut ast::Node<ast::Program>,
1804        object_id: ObjectId,
1805        command: AstMutateCommand,
1806    ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
1807        let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1808            msg: format!("Object not found: {object_id:?}"),
1809        })?;
1810        match &sketch_object.source {
1811            SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
1812            SourceRef::BackTrace { .. } => Err(Error {
1813                msg: "BackTrace source refs not supported yet".to_owned(),
1814            }),
1815        }
1816    }
1817}
1818
1819fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
1820    match source_ref {
1821        SourceRef::Simple { range } => Ok(*range),
1822        SourceRef::BackTrace { ranges } => {
1823            if ranges.len() != 1 {
1824                return Err(Error {
1825                    msg: format!(
1826                        "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
1827                        ranges.len(),
1828                    ),
1829                });
1830            }
1831            Ok(ranges[0])
1832        }
1833    }
1834}
1835
1836/// Return the AST expression referencing the variable at the given source ref.
1837/// If no such variable exists, insert a new variable declaration with the given
1838/// prefix.
1839///
1840/// This may return a complex expression referencing properties of the variable
1841/// (e.g., `line1.start`).
1842fn get_or_insert_ast_reference(
1843    ast: &mut ast::Node<ast::Program>,
1844    source_ref: &SourceRef,
1845    prefix: &str,
1846    property: Option<&str>,
1847) -> api::Result<ast::Expr> {
1848    let range = expect_single_source_range(source_ref)?;
1849    let command = AstMutateCommand::AddVariableDeclaration {
1850        prefix: prefix.to_owned(),
1851    };
1852    let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
1853    let AstMutateCommandReturn::Name(var_name) = ret else {
1854        return Err(Error {
1855            msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
1856        });
1857    };
1858    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
1859    let Some(property) = property else {
1860        // No property; just return the variable name.
1861        return Ok(var_expr);
1862    };
1863
1864    Ok(ast::Expr::MemberExpression(Box::new(ast::Node::no_src(
1865        ast::MemberExpression {
1866            object: var_expr,
1867            property: ast::Expr::Name(Box::new(ast::Name::new(property))),
1868            computed: false,
1869            digest: None,
1870        },
1871    ))))
1872}
1873
1874fn mutate_ast_node_by_source_range(
1875    ast: &mut ast::Node<ast::Program>,
1876    source_range: SourceRange,
1877    command: AstMutateCommand,
1878) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
1879    let mut context = AstMutateContext {
1880        source_range,
1881        command,
1882        defined_names_stack: Default::default(),
1883    };
1884    let control = dfs_mut(ast, &mut context);
1885    match control {
1886        ControlFlow::Continue(_) => Err(Error {
1887            msg: format!("Source range not found: {source_range:?}"),
1888        }),
1889        ControlFlow::Break(break_value) => break_value,
1890    }
1891}
1892
1893#[derive(Debug)]
1894struct AstMutateContext {
1895    source_range: SourceRange,
1896    command: AstMutateCommand,
1897    defined_names_stack: Vec<HashSet<String>>,
1898}
1899
1900#[derive(Debug)]
1901#[allow(clippy::large_enum_variant)]
1902enum AstMutateCommand {
1903    /// Add an expression statement to the sketch block.
1904    AddSketchBlockExprStmt {
1905        expr: ast::Expr,
1906    },
1907    AddVariableDeclaration {
1908        prefix: String,
1909    },
1910    EditPoint {
1911        at: ast::Expr,
1912    },
1913    EditLine {
1914        start: ast::Expr,
1915        end: ast::Expr,
1916    },
1917    EditArc {
1918        start: ast::Expr,
1919        end: ast::Expr,
1920        center: ast::Expr,
1921    },
1922    #[cfg(feature = "artifact-graph")]
1923    EditVarInitialValue {
1924        value: Number,
1925    },
1926    DeleteNode,
1927}
1928
1929#[derive(Debug)]
1930enum AstMutateCommandReturn {
1931    None,
1932    Name(String),
1933}
1934
1935impl Visitor for AstMutateContext {
1936    type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
1937    type Continue = ();
1938
1939    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
1940        filter_and_process(self, node)
1941    }
1942
1943    fn finish(&mut self, node: NodeMut<'_>) {
1944        match &node {
1945            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
1946                self.defined_names_stack.pop();
1947            }
1948            _ => {}
1949        }
1950    }
1951}
1952fn filter_and_process(
1953    ctx: &mut AstMutateContext,
1954    node: NodeMut,
1955) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
1956    let Ok(node_range) = SourceRange::try_from(&node) else {
1957        // Nodes that can't be converted to a range aren't interesting.
1958        return TraversalReturn::new_continue(());
1959    };
1960    // If we're adding a variable declaration, we need to look at variable
1961    // declaration expressions to see if it already has a variable, before
1962    // continuing. The variable declaration's source range won't match the
1963    // target; its init expression will.
1964    if let NodeMut::VariableDeclaration(var_decl) = &node {
1965        let expr_range = SourceRange::from(&var_decl.declaration.init);
1966        if expr_range == ctx.source_range {
1967            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
1968                // We found the variable declaration expression. It doesn't need
1969                // to be added.
1970                return TraversalReturn::new_break(Ok((
1971                    node_range,
1972                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
1973                )));
1974            }
1975            if let AstMutateCommand::DeleteNode = &ctx.command {
1976                // We found the variable declaration. Delete the variable along
1977                // with the segment.
1978                return TraversalReturn {
1979                    mutate_body_item: MutateBodyItem::Delete,
1980                    control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
1981                };
1982            }
1983        }
1984    }
1985
1986    if let NodeMut::Program(program) = &node {
1987        ctx.defined_names_stack.push(find_defined_names(*program));
1988    } else if let NodeMut::SketchBlock(block) = &node {
1989        ctx.defined_names_stack.push(find_defined_names(&block.body));
1990    }
1991
1992    // Make sure the node matches the source range.
1993    if node_range != ctx.source_range {
1994        return TraversalReturn::new_continue(());
1995    }
1996    process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
1997}
1998
1999fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2000    match &ctx.command {
2001        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2002            if let NodeMut::SketchBlock(sketch_block) = node {
2003                sketch_block
2004                    .body
2005                    .items
2006                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
2007                        inner: ast::ExpressionStatement {
2008                            expression: expr.clone(),
2009                            digest: None,
2010                        },
2011                        start: Default::default(),
2012                        end: Default::default(),
2013                        module_id: Default::default(),
2014                        outer_attrs: Default::default(),
2015                        pre_comments: Default::default(),
2016                        comment_start: Default::default(),
2017                    }));
2018                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2019            }
2020        }
2021        AstMutateCommand::AddVariableDeclaration { prefix } => {
2022            if let NodeMut::VariableDeclaration(inner) = node {
2023                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
2024            }
2025            if let NodeMut::ExpressionStatement(expr_stmt) = node {
2026                let empty_defined_names = HashSet::new();
2027                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
2028                let Ok(name) = next_free_name(prefix, defined_names) else {
2029                    // TODO: Return an error instead?
2030                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2031                };
2032                let mutate_node =
2033                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
2034                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
2035                        ast::ItemVisibility::Default,
2036                        ast::VariableKind::Const,
2037                    ))));
2038                return TraversalReturn {
2039                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
2040                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
2041                };
2042            }
2043        }
2044        AstMutateCommand::EditPoint { at } => {
2045            if let NodeMut::CallExpressionKw(call) = node {
2046                if call.callee.name.name != POINT_FN {
2047                    return TraversalReturn::new_continue(());
2048                }
2049                // Update the arguments.
2050                for labeled_arg in &mut call.arguments {
2051                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
2052                        labeled_arg.arg = at.clone();
2053                    }
2054                }
2055                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2056            }
2057        }
2058        AstMutateCommand::EditLine { start, end } => {
2059            if let NodeMut::CallExpressionKw(call) = node {
2060                if call.callee.name.name != LINE_FN {
2061                    return TraversalReturn::new_continue(());
2062                }
2063                // Update the arguments.
2064                for labeled_arg in &mut call.arguments {
2065                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
2066                        labeled_arg.arg = start.clone();
2067                    }
2068                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
2069                        labeled_arg.arg = end.clone();
2070                    }
2071                }
2072                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2073            }
2074        }
2075        AstMutateCommand::EditArc { start, end, center } => {
2076            if let NodeMut::CallExpressionKw(call) = node {
2077                if call.callee.name.name != ARC_FN {
2078                    return TraversalReturn::new_continue(());
2079                }
2080                // Update the arguments.
2081                for labeled_arg in &mut call.arguments {
2082                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
2083                        labeled_arg.arg = start.clone();
2084                    }
2085                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
2086                        labeled_arg.arg = end.clone();
2087                    }
2088                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
2089                        labeled_arg.arg = center.clone();
2090                    }
2091                }
2092                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2093            }
2094        }
2095        #[cfg(feature = "artifact-graph")]
2096        AstMutateCommand::EditVarInitialValue { value } => {
2097            if let NodeMut::NumericLiteral(numeric_literal) = node {
2098                // Update the initial value.
2099                let Ok(literal) = to_source_number(*value) else {
2100                    return TraversalReturn::new_break(Err(Error {
2101                        msg: format!("Could not convert number to AST literal: {:?}", *value),
2102                    }));
2103                };
2104                *numeric_literal = ast::Node::no_src(literal);
2105                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2106            }
2107        }
2108        AstMutateCommand::DeleteNode => {
2109            return TraversalReturn {
2110                mutate_body_item: MutateBodyItem::Delete,
2111                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
2112            };
2113        }
2114    }
2115    TraversalReturn::new_continue(())
2116}
2117
2118struct FindSketchBlockSourceRange {
2119    /// The source range of the sketch block before mutation.
2120    target_before_mutation: SourceRange,
2121    /// The source range of the sketch block's last body item after mutation. We
2122    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
2123    /// shared reference.
2124    found: Cell<Option<SourceRange>>,
2125}
2126
2127impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
2128    type Error = crate::front::Error;
2129
2130    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
2131        let Ok(node_range) = SourceRange::try_from(&node) else {
2132            return Ok(true);
2133        };
2134
2135        if let crate::walk::Node::SketchBlock(sketch_block) = node {
2136            if node_range.module_id() == self.target_before_mutation.module_id()
2137                && node_range.start() == self.target_before_mutation.start()
2138                // End shouldn't match since we added something.
2139                && node_range.end() >= self.target_before_mutation.end()
2140            {
2141                self.found.set(sketch_block.body.items.last().map(SourceRange::from));
2142                return Ok(false);
2143            } else {
2144                // We found a different sketch block. No need to descend into
2145                // its children since sketch blocks cannot be nested.
2146                return Ok(true);
2147            }
2148        }
2149
2150        for child in node.children().iter() {
2151            if !child.visit(*self)? {
2152                return Ok(false);
2153            }
2154        }
2155
2156        Ok(true)
2157    }
2158}
2159
2160/// After adding an item to a sketch block, find the sketch block, and get the
2161/// source range of the added item. We assume that the added item is the last
2162/// item in the sketch block and that the sketch block's source range has grown,
2163/// but not moved from its starting offset.
2164///
2165/// TODO: Do we need to format *before* mutation in case formatting moves the
2166/// sketch block forward?
2167fn find_sketch_block_added_item(
2168    ast: &ast::Node<ast::Program>,
2169    range_before_mutation: SourceRange,
2170) -> api::Result<SourceRange> {
2171    let find = FindSketchBlockSourceRange {
2172        target_before_mutation: range_before_mutation,
2173        found: Cell::new(None),
2174    };
2175    let node = crate::walk::Node::from(ast);
2176    node.visit(&find)?;
2177    find.found.into_inner().ok_or_else(|| api::Error {
2178        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?"),
2179    })
2180}
2181
2182fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
2183    // TODO: Don't duplicate this from lib.rs Program.
2184    ast.recast_top(&Default::default(), 0)
2185}
2186
2187fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
2188    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
2189        inner: ast::ArrayExpression {
2190            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
2191            non_code_meta: Default::default(),
2192            digest: None,
2193        },
2194        start: Default::default(),
2195        end: Default::default(),
2196        module_id: Default::default(),
2197        outer_attrs: Default::default(),
2198        pre_comments: Default::default(),
2199        comment_start: Default::default(),
2200    })))
2201}
2202
2203fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
2204    match expr {
2205        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
2206            inner: ast::Literal::from(to_source_number(*number)?),
2207            start: Default::default(),
2208            end: Default::default(),
2209            module_id: Default::default(),
2210            outer_attrs: Default::default(),
2211            pre_comments: Default::default(),
2212            comment_start: Default::default(),
2213        }))),
2214        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
2215            inner: ast::SketchVar {
2216                initial: Some(Box::new(ast::Node {
2217                    inner: to_source_number(*number)?,
2218                    start: Default::default(),
2219                    end: Default::default(),
2220                    module_id: Default::default(),
2221                    outer_attrs: Default::default(),
2222                    pre_comments: Default::default(),
2223                    comment_start: Default::default(),
2224                })),
2225                digest: None,
2226            },
2227            start: Default::default(),
2228            end: Default::default(),
2229            module_id: Default::default(),
2230            outer_attrs: Default::default(),
2231            pre_comments: Default::default(),
2232            comment_start: Default::default(),
2233        }))),
2234        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
2235    }
2236}
2237
2238fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
2239    Ok(ast::NumericLiteral {
2240        value: number.value,
2241        suffix: number.units,
2242        raw: format_number_literal(number.value, number.units)?,
2243        digest: None,
2244    })
2245}
2246
2247fn ast_name_expr(name: String) -> ast::Expr {
2248    ast::Expr::Name(Box::new(ast_name(name)))
2249}
2250
2251fn ast_name(name: String) -> ast::Node<ast::Name> {
2252    ast::Node {
2253        inner: ast::Name {
2254            name: ast::Node {
2255                inner: ast::Identifier { name, digest: None },
2256                start: Default::default(),
2257                end: Default::default(),
2258                module_id: Default::default(),
2259                outer_attrs: Default::default(),
2260                pre_comments: Default::default(),
2261                comment_start: Default::default(),
2262            },
2263            path: Vec::new(),
2264            abs_path: false,
2265            digest: None,
2266        },
2267        start: Default::default(),
2268        end: Default::default(),
2269        module_id: Default::default(),
2270        outer_attrs: Default::default(),
2271        pre_comments: Default::default(),
2272        comment_start: Default::default(),
2273    }
2274}
2275
2276fn ast_sketch2_name(name: &str) -> ast::Name {
2277    ast::Name {
2278        name: ast::Node {
2279            inner: ast::Identifier {
2280                name: name.to_owned(),
2281                digest: None,
2282            },
2283            start: Default::default(),
2284            end: Default::default(),
2285            module_id: Default::default(),
2286            outer_attrs: Default::default(),
2287            pre_comments: Default::default(),
2288            comment_start: Default::default(),
2289        },
2290        path: vec![ast::Node::no_src(ast::Identifier {
2291            name: "sketch2".to_owned(),
2292            digest: None,
2293        })],
2294        abs_path: false,
2295        digest: None,
2296    }
2297}
2298
2299#[cfg(test)]
2300mod tests {
2301    use super::*;
2302    use crate::{
2303        engine::PlaneName,
2304        front::{Distance, Plane, Sketch},
2305        frontend::sketch::Vertical,
2306        pretty::NumericSuffix,
2307    };
2308
2309    #[tokio::test(flavor = "multi_thread")]
2310    async fn test_new_sketch_add_point_edit_point() {
2311        let program = Program::empty();
2312
2313        let mut frontend = FrontendState::new();
2314        frontend.program = program;
2315
2316        let mock_ctx = ExecutorContext::new_mock(None).await;
2317        let version = Version(0);
2318
2319        let sketch_args = SketchArgs {
2320            on: api::Plane::Default(PlaneName::Xy),
2321        };
2322        let (_src_delta, scene_delta, sketch_id) = frontend
2323            .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2324            .await
2325            .unwrap();
2326        assert_eq!(sketch_id, ObjectId(0));
2327        assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2328        let sketch_object = &scene_delta.new_graph.objects[0];
2329        assert_eq!(sketch_object.id, ObjectId(0));
2330        assert_eq!(
2331            sketch_object.kind,
2332            ObjectKind::Sketch(Sketch {
2333                args: SketchArgs {
2334                    on: Plane::Default(PlaneName::Xy)
2335                },
2336                segments: vec![],
2337                constraints: vec![],
2338            })
2339        );
2340        assert_eq!(scene_delta.new_graph.objects.len(), 1);
2341
2342        let point_ctor = PointCtor {
2343            position: Point2d {
2344                x: Expr::Number(Number {
2345                    value: 1.0,
2346                    units: NumericSuffix::Inch,
2347                }),
2348                y: Expr::Number(Number {
2349                    value: 2.0,
2350                    units: NumericSuffix::Inch,
2351                }),
2352            },
2353        };
2354        let segment = SegmentCtor::Point(point_ctor);
2355        let (src_delta, scene_delta) = frontend
2356            .add_segment(&mock_ctx, version, sketch_id, segment, None)
2357            .await
2358            .unwrap();
2359        assert_eq!(
2360            src_delta.text.as_str(),
2361            "@settings(experimentalFeatures = allow)
2362
2363sketch(on = XY) {
2364  sketch2::point(at = [1in, 2in])
2365}
2366"
2367        );
2368        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
2369        assert_eq!(scene_delta.new_graph.objects.len(), 2);
2370        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2371            assert_eq!(scene_object.id.0, i);
2372        }
2373        assert_eq!(scene_delta.new_graph.objects.len(), 2);
2374
2375        let point_id = *scene_delta.new_objects.last().unwrap();
2376
2377        let point_ctor = PointCtor {
2378            position: Point2d {
2379                x: Expr::Number(Number {
2380                    value: 3.0,
2381                    units: NumericSuffix::Inch,
2382                }),
2383                y: Expr::Number(Number {
2384                    value: 4.0,
2385                    units: NumericSuffix::Inch,
2386                }),
2387            },
2388        };
2389        let segments = vec![ExistingSegmentCtor {
2390            id: point_id,
2391            ctor: SegmentCtor::Point(point_ctor),
2392        }];
2393        let (src_delta, scene_delta) = frontend
2394            .edit_segments(&mock_ctx, version, sketch_id, segments)
2395            .await
2396            .unwrap();
2397        assert_eq!(
2398            src_delta.text.as_str(),
2399            "@settings(experimentalFeatures = allow)
2400
2401sketch(on = XY) {
2402  sketch2::point(at = [3in, 4in])
2403}
2404"
2405        );
2406        assert_eq!(scene_delta.new_objects, vec![]);
2407        assert_eq!(scene_delta.new_graph.objects.len(), 2);
2408
2409        mock_ctx.close().await;
2410    }
2411
2412    #[tokio::test(flavor = "multi_thread")]
2413    async fn test_new_sketch_add_line_edit_line() {
2414        let program = Program::empty();
2415
2416        let mut frontend = FrontendState::new();
2417        frontend.program = program;
2418
2419        let mock_ctx = ExecutorContext::new_mock(None).await;
2420        let version = Version(0);
2421
2422        let sketch_args = SketchArgs {
2423            on: api::Plane::Default(PlaneName::Xy),
2424        };
2425        let (_src_delta, scene_delta, sketch_id) = frontend
2426            .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2427            .await
2428            .unwrap();
2429        assert_eq!(sketch_id, ObjectId(0));
2430        assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2431        let sketch_object = &scene_delta.new_graph.objects[0];
2432        assert_eq!(sketch_object.id, ObjectId(0));
2433        assert_eq!(
2434            sketch_object.kind,
2435            ObjectKind::Sketch(Sketch {
2436                args: SketchArgs {
2437                    on: Plane::Default(PlaneName::Xy)
2438                },
2439                segments: vec![],
2440                constraints: vec![],
2441            })
2442        );
2443        assert_eq!(scene_delta.new_graph.objects.len(), 1);
2444
2445        let line_ctor = LineCtor {
2446            start: Point2d {
2447                x: Expr::Number(Number {
2448                    value: 0.0,
2449                    units: NumericSuffix::Mm,
2450                }),
2451                y: Expr::Number(Number {
2452                    value: 0.0,
2453                    units: NumericSuffix::Mm,
2454                }),
2455            },
2456            end: Point2d {
2457                x: Expr::Number(Number {
2458                    value: 10.0,
2459                    units: NumericSuffix::Mm,
2460                }),
2461                y: Expr::Number(Number {
2462                    value: 10.0,
2463                    units: NumericSuffix::Mm,
2464                }),
2465            },
2466        };
2467        let segment = SegmentCtor::Line(line_ctor);
2468        let (src_delta, scene_delta) = frontend
2469            .add_segment(&mock_ctx, version, sketch_id, segment, None)
2470            .await
2471            .unwrap();
2472        assert_eq!(
2473            src_delta.text.as_str(),
2474            "@settings(experimentalFeatures = allow)
2475
2476sketch(on = XY) {
2477  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2478}
2479"
2480        );
2481        assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2482        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2483            assert_eq!(scene_object.id.0, i);
2484        }
2485        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2486
2487        // The new objects are the end points and then the line.
2488        let line = *scene_delta.new_objects.last().unwrap();
2489
2490        let line_ctor = LineCtor {
2491            start: Point2d {
2492                x: Expr::Number(Number {
2493                    value: 1.0,
2494                    units: NumericSuffix::Mm,
2495                }),
2496                y: Expr::Number(Number {
2497                    value: 2.0,
2498                    units: NumericSuffix::Mm,
2499                }),
2500            },
2501            end: Point2d {
2502                x: Expr::Number(Number {
2503                    value: 13.0,
2504                    units: NumericSuffix::Mm,
2505                }),
2506                y: Expr::Number(Number {
2507                    value: 14.0,
2508                    units: NumericSuffix::Mm,
2509                }),
2510            },
2511        };
2512        let segments = vec![ExistingSegmentCtor {
2513            id: line,
2514            ctor: SegmentCtor::Line(line_ctor),
2515        }];
2516        let (src_delta, scene_delta) = frontend
2517            .edit_segments(&mock_ctx, version, sketch_id, segments)
2518            .await
2519            .unwrap();
2520        assert_eq!(
2521            src_delta.text.as_str(),
2522            "@settings(experimentalFeatures = allow)
2523
2524sketch(on = XY) {
2525  sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
2526}
2527"
2528        );
2529        assert_eq!(scene_delta.new_objects, vec![]);
2530        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2531
2532        mock_ctx.close().await;
2533    }
2534
2535    #[tokio::test(flavor = "multi_thread")]
2536    async fn test_new_sketch_add_arc_edit_arc() {
2537        let program = Program::empty();
2538
2539        let mut frontend = FrontendState::new();
2540        frontend.program = program;
2541
2542        let mock_ctx = ExecutorContext::new_mock(None).await;
2543        let version = Version(0);
2544
2545        let sketch_args = SketchArgs {
2546            on: api::Plane::Default(PlaneName::Xy),
2547        };
2548        let (_src_delta, scene_delta, sketch_id) = frontend
2549            .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2550            .await
2551            .unwrap();
2552        assert_eq!(sketch_id, ObjectId(0));
2553        assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2554        let sketch_object = &scene_delta.new_graph.objects[0];
2555        assert_eq!(sketch_object.id, ObjectId(0));
2556        assert_eq!(
2557            sketch_object.kind,
2558            ObjectKind::Sketch(Sketch {
2559                args: SketchArgs {
2560                    on: Plane::Default(PlaneName::Xy)
2561                },
2562                segments: vec![],
2563                constraints: vec![],
2564            })
2565        );
2566        assert_eq!(scene_delta.new_graph.objects.len(), 1);
2567
2568        let arc_ctor = ArcCtor {
2569            start: Point2d {
2570                x: Expr::Var(Number {
2571                    value: 0.0,
2572                    units: NumericSuffix::Mm,
2573                }),
2574                y: Expr::Var(Number {
2575                    value: 0.0,
2576                    units: NumericSuffix::Mm,
2577                }),
2578            },
2579            end: Point2d {
2580                x: Expr::Var(Number {
2581                    value: 10.0,
2582                    units: NumericSuffix::Mm,
2583                }),
2584                y: Expr::Var(Number {
2585                    value: 10.0,
2586                    units: NumericSuffix::Mm,
2587                }),
2588            },
2589            center: Point2d {
2590                x: Expr::Var(Number {
2591                    value: 10.0,
2592                    units: NumericSuffix::Mm,
2593                }),
2594                y: Expr::Var(Number {
2595                    value: 0.0,
2596                    units: NumericSuffix::Mm,
2597                }),
2598            },
2599        };
2600        let segment = SegmentCtor::Arc(arc_ctor);
2601        let (src_delta, scene_delta) = frontend
2602            .add_segment(&mock_ctx, version, sketch_id, segment, None)
2603            .await
2604            .unwrap();
2605        assert_eq!(
2606            src_delta.text.as_str(),
2607            "@settings(experimentalFeatures = allow)
2608
2609sketch(on = XY) {
2610  sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
2611}
2612"
2613        );
2614        assert_eq!(
2615            scene_delta.new_objects,
2616            vec![ObjectId(1), ObjectId(2), ObjectId(3), ObjectId(4)]
2617        );
2618        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2619            assert_eq!(scene_object.id.0, i);
2620        }
2621        assert_eq!(scene_delta.new_graph.objects.len(), 5);
2622
2623        // The new objects are the end points, the center, and then the arc.
2624        let arc = *scene_delta.new_objects.last().unwrap();
2625
2626        let arc_ctor = ArcCtor {
2627            start: Point2d {
2628                x: Expr::Var(Number {
2629                    value: 1.0,
2630                    units: NumericSuffix::Mm,
2631                }),
2632                y: Expr::Var(Number {
2633                    value: 2.0,
2634                    units: NumericSuffix::Mm,
2635                }),
2636            },
2637            end: Point2d {
2638                x: Expr::Var(Number {
2639                    value: 13.0,
2640                    units: NumericSuffix::Mm,
2641                }),
2642                y: Expr::Var(Number {
2643                    value: 14.0,
2644                    units: NumericSuffix::Mm,
2645                }),
2646            },
2647            center: Point2d {
2648                x: Expr::Var(Number {
2649                    value: 13.0,
2650                    units: NumericSuffix::Mm,
2651                }),
2652                y: Expr::Var(Number {
2653                    value: 2.0,
2654                    units: NumericSuffix::Mm,
2655                }),
2656            },
2657        };
2658        let segments = vec![ExistingSegmentCtor {
2659            id: arc,
2660            ctor: SegmentCtor::Arc(arc_ctor),
2661        }];
2662        let (src_delta, scene_delta) = frontend
2663            .edit_segments(&mock_ctx, version, sketch_id, segments)
2664            .await
2665            .unwrap();
2666        assert_eq!(
2667            src_delta.text.as_str(),
2668            "@settings(experimentalFeatures = allow)
2669
2670sketch(on = XY) {
2671  sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
2672}
2673"
2674        );
2675        assert_eq!(scene_delta.new_objects, vec![]);
2676        assert_eq!(scene_delta.new_graph.objects.len(), 5);
2677
2678        mock_ctx.close().await;
2679    }
2680
2681    #[tokio::test(flavor = "multi_thread")]
2682    async fn test_add_line_when_sketch_block_uses_variable() {
2683        let initial_source = "@settings(experimentalFeatures = allow)
2684
2685s = sketch(on = XY) {}
2686";
2687
2688        let program = Program::parse(initial_source).unwrap().0.unwrap();
2689
2690        let mut frontend = FrontendState::new();
2691
2692        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2693        let mock_ctx = ExecutorContext::new_mock(None).await;
2694        let version = Version(0);
2695
2696        frontend.hack_set_program(&ctx, program).await.unwrap();
2697        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2698
2699        let line_ctor = LineCtor {
2700            start: Point2d {
2701                x: Expr::Number(Number {
2702                    value: 0.0,
2703                    units: NumericSuffix::Mm,
2704                }),
2705                y: Expr::Number(Number {
2706                    value: 0.0,
2707                    units: NumericSuffix::Mm,
2708                }),
2709            },
2710            end: Point2d {
2711                x: Expr::Number(Number {
2712                    value: 10.0,
2713                    units: NumericSuffix::Mm,
2714                }),
2715                y: Expr::Number(Number {
2716                    value: 10.0,
2717                    units: NumericSuffix::Mm,
2718                }),
2719            },
2720        };
2721        let segment = SegmentCtor::Line(line_ctor);
2722        let (src_delta, scene_delta) = frontend
2723            .add_segment(&mock_ctx, version, sketch_id, segment, None)
2724            .await
2725            .unwrap();
2726        assert_eq!(
2727            src_delta.text.as_str(),
2728            "@settings(experimentalFeatures = allow)
2729
2730s = sketch(on = XY) {
2731  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2732}
2733"
2734        );
2735        assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2736        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2737
2738        ctx.close().await;
2739        mock_ctx.close().await;
2740    }
2741
2742    #[tokio::test(flavor = "multi_thread")]
2743    async fn test_new_sketch_add_line_delete_sketch() {
2744        let program = Program::empty();
2745
2746        let mut frontend = FrontendState::new();
2747        frontend.program = program;
2748
2749        let mock_ctx = ExecutorContext::new_mock(None).await;
2750        let version = Version(0);
2751
2752        let sketch_args = SketchArgs {
2753            on: api::Plane::Default(PlaneName::Xy),
2754        };
2755        let (_src_delta, scene_delta, sketch_id) = frontend
2756            .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2757            .await
2758            .unwrap();
2759        assert_eq!(sketch_id, ObjectId(0));
2760        assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2761        let sketch_object = &scene_delta.new_graph.objects[0];
2762        assert_eq!(sketch_object.id, ObjectId(0));
2763        assert_eq!(
2764            sketch_object.kind,
2765            ObjectKind::Sketch(Sketch {
2766                args: SketchArgs {
2767                    on: Plane::Default(PlaneName::Xy)
2768                },
2769                segments: vec![],
2770                constraints: vec![],
2771            })
2772        );
2773        assert_eq!(scene_delta.new_graph.objects.len(), 1);
2774
2775        let line_ctor = LineCtor {
2776            start: Point2d {
2777                x: Expr::Number(Number {
2778                    value: 0.0,
2779                    units: NumericSuffix::Mm,
2780                }),
2781                y: Expr::Number(Number {
2782                    value: 0.0,
2783                    units: NumericSuffix::Mm,
2784                }),
2785            },
2786            end: Point2d {
2787                x: Expr::Number(Number {
2788                    value: 10.0,
2789                    units: NumericSuffix::Mm,
2790                }),
2791                y: Expr::Number(Number {
2792                    value: 10.0,
2793                    units: NumericSuffix::Mm,
2794                }),
2795            },
2796        };
2797        let segment = SegmentCtor::Line(line_ctor);
2798        let (src_delta, scene_delta) = frontend
2799            .add_segment(&mock_ctx, version, sketch_id, segment, None)
2800            .await
2801            .unwrap();
2802        assert_eq!(
2803            src_delta.text.as_str(),
2804            "@settings(experimentalFeatures = allow)
2805
2806sketch(on = XY) {
2807  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2808}
2809"
2810        );
2811        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2812
2813        let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
2814        assert_eq!(
2815            src_delta.text.as_str(),
2816            "@settings(experimentalFeatures = allow)
2817"
2818        );
2819        assert_eq!(scene_delta.new_graph.objects.len(), 0);
2820
2821        mock_ctx.close().await;
2822    }
2823
2824    #[tokio::test(flavor = "multi_thread")]
2825    async fn test_delete_sketch_when_sketch_block_uses_variable() {
2826        let initial_source = "@settings(experimentalFeatures = allow)
2827
2828s = sketch(on = XY) {}
2829";
2830
2831        let program = Program::parse(initial_source).unwrap().0.unwrap();
2832
2833        let mut frontend = FrontendState::new();
2834
2835        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2836        let mock_ctx = ExecutorContext::new_mock(None).await;
2837        let version = Version(0);
2838
2839        frontend.hack_set_program(&ctx, program).await.unwrap();
2840        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2841
2842        let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
2843        assert_eq!(
2844            src_delta.text.as_str(),
2845            "@settings(experimentalFeatures = allow)
2846"
2847        );
2848        assert_eq!(scene_delta.new_graph.objects.len(), 0);
2849
2850        ctx.close().await;
2851        mock_ctx.close().await;
2852    }
2853
2854    #[tokio::test(flavor = "multi_thread")]
2855    async fn test_edit_line_when_editing_its_start_point() {
2856        let initial_source = "\
2857@settings(experimentalFeatures = allow)
2858
2859sketch(on = XY) {
2860  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2861}
2862";
2863
2864        let program = Program::parse(initial_source).unwrap().0.unwrap();
2865
2866        let mut frontend = FrontendState::new();
2867
2868        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2869        let mock_ctx = ExecutorContext::new_mock(None).await;
2870        let version = Version(0);
2871
2872        frontend.hack_set_program(&ctx, program).await.unwrap();
2873        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2874
2875        let point_id = frontend.scene_graph.objects.get(1).unwrap().id;
2876
2877        let point_ctor = PointCtor {
2878            position: Point2d {
2879                x: Expr::Var(Number {
2880                    value: 5.0,
2881                    units: NumericSuffix::Inch,
2882                }),
2883                y: Expr::Var(Number {
2884                    value: 6.0,
2885                    units: NumericSuffix::Inch,
2886                }),
2887            },
2888        };
2889        let segments = vec![ExistingSegmentCtor {
2890            id: point_id,
2891            ctor: SegmentCtor::Point(point_ctor),
2892        }];
2893        let (src_delta, scene_delta) = frontend
2894            .edit_segments(&mock_ctx, version, sketch_id, segments)
2895            .await
2896            .unwrap();
2897        assert_eq!(
2898            src_delta.text.as_str(),
2899            "\
2900@settings(experimentalFeatures = allow)
2901
2902sketch(on = XY) {
2903  sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
2904}
2905"
2906        );
2907        assert_eq!(scene_delta.new_objects, vec![]);
2908        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2909
2910        ctx.close().await;
2911        mock_ctx.close().await;
2912    }
2913
2914    #[tokio::test(flavor = "multi_thread")]
2915    async fn test_edit_line_when_editing_its_end_point() {
2916        let initial_source = "\
2917@settings(experimentalFeatures = allow)
2918
2919sketch(on = XY) {
2920  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2921}
2922";
2923
2924        let program = Program::parse(initial_source).unwrap().0.unwrap();
2925
2926        let mut frontend = FrontendState::new();
2927
2928        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2929        let mock_ctx = ExecutorContext::new_mock(None).await;
2930        let version = Version(0);
2931
2932        frontend.hack_set_program(&ctx, program).await.unwrap();
2933        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2934
2935        let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
2936
2937        let point_ctor = PointCtor {
2938            position: Point2d {
2939                x: Expr::Var(Number {
2940                    value: 5.0,
2941                    units: NumericSuffix::Inch,
2942                }),
2943                y: Expr::Var(Number {
2944                    value: 6.0,
2945                    units: NumericSuffix::Inch,
2946                }),
2947            },
2948        };
2949        let segments = vec![ExistingSegmentCtor {
2950            id: point_id,
2951            ctor: SegmentCtor::Point(point_ctor),
2952        }];
2953        let (src_delta, scene_delta) = frontend
2954            .edit_segments(&mock_ctx, version, sketch_id, segments)
2955            .await
2956            .unwrap();
2957        assert_eq!(
2958            src_delta.text.as_str(),
2959            "\
2960@settings(experimentalFeatures = allow)
2961
2962sketch(on = XY) {
2963  sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
2964}
2965"
2966        );
2967        assert_eq!(scene_delta.new_objects, vec![]);
2968        assert_eq!(scene_delta.new_graph.objects.len(), 4);
2969
2970        ctx.close().await;
2971        mock_ctx.close().await;
2972    }
2973
2974    #[tokio::test(flavor = "multi_thread")]
2975    async fn test_edit_line_with_coincident_feedback() {
2976        let initial_source = "\
2977@settings(experimentalFeatures = allow)
2978
2979sketch(on = XY) {
2980  line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
2981  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2982  line1.start.at[0] == 0
2983  line1.start.at[1] == 0
2984  sketch2::coincident([line1.end, line2.start])
2985  sketch2::equalLength([line1, line2])
2986}
2987";
2988
2989        let program = Program::parse(initial_source).unwrap().0.unwrap();
2990
2991        let mut frontend = FrontendState::new();
2992
2993        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2994        let mock_ctx = ExecutorContext::new_mock(None).await;
2995        let version = Version(0);
2996
2997        frontend.hack_set_program(&ctx, program).await.unwrap();
2998        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2999        let line2_end_id = frontend.scene_graph.objects.get(5).unwrap().id;
3000
3001        let segments = vec![ExistingSegmentCtor {
3002            id: line2_end_id,
3003            ctor: SegmentCtor::Point(PointCtor {
3004                position: Point2d {
3005                    x: Expr::Var(Number {
3006                        value: 9.0,
3007                        units: NumericSuffix::None,
3008                    }),
3009                    y: Expr::Var(Number {
3010                        value: 10.0,
3011                        units: NumericSuffix::None,
3012                    }),
3013                },
3014            }),
3015        }];
3016        let (src_delta, scene_delta) = frontend
3017            .edit_segments(&mock_ctx, version, sketch_id, segments)
3018            .await
3019            .unwrap();
3020        assert_eq!(
3021            src_delta.text.as_str(),
3022            "\
3023@settings(experimentalFeatures = allow)
3024
3025sketch(on = XY) {
3026  line1 = sketch2::line(start = [var -0mm, var -0mm], end = [var 4.145mm, var 5.32mm])
3027  line2 = sketch2::line(start = [var 4.145mm, var 5.32mm], end = [var 9mm, var 10mm])
3028line1.start.at[0] == 0
3029line1.start.at[1] == 0
3030  sketch2::coincident([line1.end, line2.start])
3031  sketch2::equalLength([line1, line2])
3032}
3033"
3034        );
3035        assert_eq!(
3036            scene_delta.new_graph.objects.len(),
3037            9,
3038            "{:#?}",
3039            scene_delta.new_graph.objects
3040        );
3041
3042        ctx.close().await;
3043        mock_ctx.close().await;
3044    }
3045
3046    #[tokio::test(flavor = "multi_thread")]
3047    async fn test_delete_point_without_var() {
3048        let initial_source = "\
3049@settings(experimentalFeatures = allow)
3050
3051sketch(on = XY) {
3052  sketch2::point(at = [var 1, var 2])
3053  sketch2::point(at = [var 3, var 4])
3054  sketch2::point(at = [var 5, var 6])
3055}
3056";
3057
3058        let program = Program::parse(initial_source).unwrap().0.unwrap();
3059
3060        let mut frontend = FrontendState::new();
3061
3062        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3063        let mock_ctx = ExecutorContext::new_mock(None).await;
3064        let version = Version(0);
3065
3066        frontend.hack_set_program(&ctx, program).await.unwrap();
3067        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3068
3069        let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3070
3071        let (src_delta, scene_delta) = frontend
3072            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3073            .await
3074            .unwrap();
3075        assert_eq!(
3076            src_delta.text.as_str(),
3077            "\
3078@settings(experimentalFeatures = allow)
3079
3080sketch(on = XY) {
3081  sketch2::point(at = [var 1mm, var 2mm])
3082  sketch2::point(at = [var 5mm, var 6mm])
3083}
3084"
3085        );
3086        assert_eq!(scene_delta.new_objects, vec![]);
3087        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3088
3089        ctx.close().await;
3090        mock_ctx.close().await;
3091    }
3092
3093    #[tokio::test(flavor = "multi_thread")]
3094    async fn test_delete_point_with_var() {
3095        let initial_source = "\
3096@settings(experimentalFeatures = allow)
3097
3098sketch(on = XY) {
3099  sketch2::point(at = [var 1, var 2])
3100  point1 = sketch2::point(at = [var 3, var 4])
3101  sketch2::point(at = [var 5, var 6])
3102}
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_id = frontend.scene_graph.objects.first().unwrap().id;
3115
3116        let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3117
3118        let (src_delta, scene_delta) = frontend
3119            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3120            .await
3121            .unwrap();
3122        assert_eq!(
3123            src_delta.text.as_str(),
3124            "\
3125@settings(experimentalFeatures = allow)
3126
3127sketch(on = XY) {
3128  sketch2::point(at = [var 1mm, var 2mm])
3129  sketch2::point(at = [var 5mm, var 6mm])
3130}
3131"
3132        );
3133        assert_eq!(scene_delta.new_objects, vec![]);
3134        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3135
3136        ctx.close().await;
3137        mock_ctx.close().await;
3138    }
3139
3140    #[tokio::test(flavor = "multi_thread")]
3141    async fn test_delete_multiple_points() {
3142        let initial_source = "\
3143@settings(experimentalFeatures = allow)
3144
3145sketch(on = XY) {
3146  sketch2::point(at = [var 1, var 2])
3147  point1 = sketch2::point(at = [var 3, var 4])
3148  sketch2::point(at = [var 5, var 6])
3149}
3150";
3151
3152        let program = Program::parse(initial_source).unwrap().0.unwrap();
3153
3154        let mut frontend = FrontendState::new();
3155
3156        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3157        let mock_ctx = ExecutorContext::new_mock(None).await;
3158        let version = Version(0);
3159
3160        frontend.hack_set_program(&ctx, program).await.unwrap();
3161        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3162
3163        let point1_id = frontend.scene_graph.objects.get(1).unwrap().id;
3164        let point2_id = frontend.scene_graph.objects.get(2).unwrap().id;
3165
3166        let (src_delta, scene_delta) = frontend
3167            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
3168            .await
3169            .unwrap();
3170        assert_eq!(
3171            src_delta.text.as_str(),
3172            "\
3173@settings(experimentalFeatures = allow)
3174
3175sketch(on = XY) {
3176  sketch2::point(at = [var 5mm, var 6mm])
3177}
3178"
3179        );
3180        assert_eq!(scene_delta.new_objects, vec![]);
3181        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3182
3183        ctx.close().await;
3184        mock_ctx.close().await;
3185    }
3186
3187    #[tokio::test(flavor = "multi_thread")]
3188    async fn test_delete_coincident_constraint() {
3189        let initial_source = "\
3190@settings(experimentalFeatures = allow)
3191
3192sketch(on = XY) {
3193  point1 = sketch2::point(at = [var 1, var 2])
3194  point2 = sketch2::point(at = [var 3, var 4])
3195  sketch2::coincident([point1, point2])
3196  sketch2::point(at = [var 5, var 6])
3197}
3198";
3199
3200        let program = Program::parse(initial_source).unwrap().0.unwrap();
3201
3202        let mut frontend = FrontendState::new();
3203
3204        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3205        let mock_ctx = ExecutorContext::new_mock(None).await;
3206        let version = Version(0);
3207
3208        frontend.hack_set_program(&ctx, program).await.unwrap();
3209        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3210
3211        let coincident_id = frontend.scene_graph.objects.get(3).unwrap().id;
3212
3213        let (src_delta, scene_delta) = frontend
3214            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
3215            .await
3216            .unwrap();
3217        assert_eq!(
3218            src_delta.text.as_str(),
3219            "\
3220@settings(experimentalFeatures = allow)
3221
3222sketch(on = XY) {
3223  point1 = sketch2::point(at = [var 1mm, var 2mm])
3224  point2 = sketch2::point(at = [var 3mm, var 4mm])
3225  sketch2::point(at = [var 5mm, var 6mm])
3226}
3227"
3228        );
3229        assert_eq!(scene_delta.new_objects, vec![]);
3230        assert_eq!(scene_delta.new_graph.objects.len(), 4);
3231
3232        ctx.close().await;
3233        mock_ctx.close().await;
3234    }
3235
3236    #[tokio::test(flavor = "multi_thread")]
3237    async fn test_delete_line_cascades_to_coincident_constraint() {
3238        let initial_source = "\
3239@settings(experimentalFeatures = allow)
3240
3241sketch(on = XY) {
3242  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3243  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3244  sketch2::coincident([line1.end, line2.start])
3245}
3246";
3247
3248        let program = Program::parse(initial_source).unwrap().0.unwrap();
3249
3250        let mut frontend = FrontendState::new();
3251
3252        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3253        let mock_ctx = ExecutorContext::new_mock(None).await;
3254        let version = Version(0);
3255
3256        frontend.hack_set_program(&ctx, program).await.unwrap();
3257        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3258        let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3259
3260        let (src_delta, scene_delta) = frontend
3261            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3262            .await
3263            .unwrap();
3264        assert_eq!(
3265            src_delta.text.as_str(),
3266            "\
3267@settings(experimentalFeatures = allow)
3268
3269sketch(on = XY) {
3270  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3271}
3272"
3273        );
3274        assert_eq!(
3275            scene_delta.new_graph.objects.len(),
3276            4,
3277            "{:#?}",
3278            scene_delta.new_graph.objects
3279        );
3280
3281        ctx.close().await;
3282        mock_ctx.close().await;
3283    }
3284
3285    #[tokio::test(flavor = "multi_thread")]
3286    async fn test_delete_line_cascades_to_distance_constraint() {
3287        let initial_source = "\
3288@settings(experimentalFeatures = allow)
3289
3290sketch(on = XY) {
3291  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3292  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3293  sketch2::distance([line1.end, line2.start]) == 10mm
3294}
3295";
3296
3297        let program = Program::parse(initial_source).unwrap().0.unwrap();
3298
3299        let mut frontend = FrontendState::new();
3300
3301        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3302        let mock_ctx = ExecutorContext::new_mock(None).await;
3303        let version = Version(0);
3304
3305        frontend.hack_set_program(&ctx, program).await.unwrap();
3306        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3307        let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3308
3309        let (src_delta, scene_delta) = frontend
3310            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3311            .await
3312            .unwrap();
3313        assert_eq!(
3314            src_delta.text.as_str(),
3315            "\
3316@settings(experimentalFeatures = allow)
3317
3318sketch(on = XY) {
3319  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3320}
3321"
3322        );
3323        assert_eq!(
3324            scene_delta.new_graph.objects.len(),
3325            4,
3326            "{:#?}",
3327            scene_delta.new_graph.objects
3328        );
3329
3330        ctx.close().await;
3331        mock_ctx.close().await;
3332    }
3333
3334    #[tokio::test(flavor = "multi_thread")]
3335    async fn test_two_points_coincident() {
3336        let initial_source = "\
3337@settings(experimentalFeatures = allow)
3338
3339sketch(on = XY) {
3340  point1 = sketch2::point(at = [var 1, var 2])
3341  sketch2::point(at = [3, 4])
3342}
3343";
3344
3345        let program = Program::parse(initial_source).unwrap().0.unwrap();
3346
3347        let mut frontend = FrontendState::new();
3348
3349        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3350        let mock_ctx = ExecutorContext::new_mock(None).await;
3351        let version = Version(0);
3352
3353        frontend.hack_set_program(&ctx, program).await.unwrap();
3354        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3355        let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3356        let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3357
3358        let constraint = Constraint::Coincident(Coincident {
3359            segments: vec![point0_id, point1_id],
3360        });
3361        let (src_delta, scene_delta) = frontend
3362            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3363            .await
3364            .unwrap();
3365        assert_eq!(
3366            src_delta.text.as_str(),
3367            "\
3368@settings(experimentalFeatures = allow)
3369
3370sketch(on = XY) {
3371  point1 = sketch2::point(at = [var 1, var 2])
3372  point2 = sketch2::point(at = [3, 4])
3373  sketch2::coincident([point1, point2])
3374}
3375"
3376        );
3377        assert_eq!(
3378            scene_delta.new_graph.objects.len(),
3379            4,
3380            "{:#?}",
3381            scene_delta.new_graph.objects
3382        );
3383
3384        ctx.close().await;
3385        mock_ctx.close().await;
3386    }
3387
3388    #[tokio::test(flavor = "multi_thread")]
3389    async fn test_coincident_of_line_end_points() {
3390        let initial_source = "\
3391@settings(experimentalFeatures = allow)
3392
3393sketch(on = XY) {
3394  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3395  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3396}
3397";
3398
3399        let program = Program::parse(initial_source).unwrap().0.unwrap();
3400
3401        let mut frontend = FrontendState::new();
3402
3403        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3404        let mock_ctx = ExecutorContext::new_mock(None).await;
3405        let version = Version(0);
3406
3407        frontend.hack_set_program(&ctx, program).await.unwrap();
3408        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3409        let point0_id = frontend.scene_graph.objects.get(2).unwrap().id;
3410        let point1_id = frontend.scene_graph.objects.get(4).unwrap().id;
3411
3412        let constraint = Constraint::Coincident(Coincident {
3413            segments: vec![point0_id, point1_id],
3414        });
3415        let (src_delta, scene_delta) = frontend
3416            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3417            .await
3418            .unwrap();
3419        assert_eq!(
3420            src_delta.text.as_str(),
3421            "\
3422@settings(experimentalFeatures = allow)
3423
3424sketch(on = XY) {
3425  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3426  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3427  sketch2::coincident([line1.end, line2.start])
3428}
3429"
3430        );
3431        assert_eq!(
3432            scene_delta.new_graph.objects.len(),
3433            8,
3434            "{:#?}",
3435            scene_delta.new_graph.objects
3436        );
3437
3438        ctx.close().await;
3439        mock_ctx.close().await;
3440    }
3441
3442    #[tokio::test(flavor = "multi_thread")]
3443    async fn test_distance_two_points() {
3444        let initial_source = "\
3445@settings(experimentalFeatures = allow)
3446
3447sketch(on = XY) {
3448  sketch2::point(at = [var 1, var 2])
3449  sketch2::point(at = [var 3, var 4])
3450}
3451";
3452
3453        let program = Program::parse(initial_source).unwrap().0.unwrap();
3454
3455        let mut frontend = FrontendState::new();
3456
3457        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3458        let mock_ctx = ExecutorContext::new_mock(None).await;
3459        let version = Version(0);
3460
3461        frontend.hack_set_program(&ctx, program).await.unwrap();
3462        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3463        let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3464        let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3465
3466        let constraint = Constraint::Distance(Distance {
3467            points: vec![point0_id, point1_id],
3468            distance: Number {
3469                value: 2.0,
3470                units: NumericSuffix::Mm,
3471            },
3472        });
3473        let (src_delta, scene_delta) = frontend
3474            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3475            .await
3476            .unwrap();
3477        assert_eq!(
3478            src_delta.text.as_str(),
3479            // The lack indentation is a formatter bug.
3480            "\
3481@settings(experimentalFeatures = allow)
3482
3483sketch(on = XY) {
3484  point1 = sketch2::point(at = [var 1, var 2])
3485  point2 = sketch2::point(at = [var 3, var 4])
3486sketch2::distance([point1, point2]) == 2mm
3487}
3488"
3489        );
3490        assert_eq!(
3491            scene_delta.new_graph.objects.len(),
3492            4,
3493            "{:#?}",
3494            scene_delta.new_graph.objects
3495        );
3496
3497        ctx.close().await;
3498        mock_ctx.close().await;
3499    }
3500
3501    #[tokio::test(flavor = "multi_thread")]
3502    async fn test_line_horizontal() {
3503        let initial_source = "\
3504@settings(experimentalFeatures = allow)
3505
3506sketch(on = XY) {
3507  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3508}
3509";
3510
3511        let program = Program::parse(initial_source).unwrap().0.unwrap();
3512
3513        let mut frontend = FrontendState::new();
3514
3515        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3516        let mock_ctx = ExecutorContext::new_mock(None).await;
3517        let version = Version(0);
3518
3519        frontend.hack_set_program(&ctx, program).await.unwrap();
3520        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3521        let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3522
3523        let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
3524        let (src_delta, scene_delta) = frontend
3525            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3526            .await
3527            .unwrap();
3528        assert_eq!(
3529            src_delta.text.as_str(),
3530            "\
3531@settings(experimentalFeatures = allow)
3532
3533sketch(on = XY) {
3534  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3535  sketch2::horizontal(line1)
3536}
3537"
3538        );
3539        assert_eq!(
3540            scene_delta.new_graph.objects.len(),
3541            5,
3542            "{:#?}",
3543            scene_delta.new_graph.objects
3544        );
3545
3546        ctx.close().await;
3547        mock_ctx.close().await;
3548    }
3549
3550    #[tokio::test(flavor = "multi_thread")]
3551    async fn test_line_vertical() {
3552        let initial_source = "\
3553@settings(experimentalFeatures = allow)
3554
3555sketch(on = XY) {
3556  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3557}
3558";
3559
3560        let program = Program::parse(initial_source).unwrap().0.unwrap();
3561
3562        let mut frontend = FrontendState::new();
3563
3564        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3565        let mock_ctx = ExecutorContext::new_mock(None).await;
3566        let version = Version(0);
3567
3568        frontend.hack_set_program(&ctx, program).await.unwrap();
3569        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3570        let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3571
3572        let constraint = Constraint::Vertical(Vertical { line: line1_id });
3573        let (src_delta, scene_delta) = frontend
3574            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3575            .await
3576            .unwrap();
3577        assert_eq!(
3578            src_delta.text.as_str(),
3579            "\
3580@settings(experimentalFeatures = allow)
3581
3582sketch(on = XY) {
3583  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3584  sketch2::vertical(line1)
3585}
3586"
3587        );
3588        assert_eq!(
3589            scene_delta.new_graph.objects.len(),
3590            5,
3591            "{:#?}",
3592            scene_delta.new_graph.objects
3593        );
3594
3595        ctx.close().await;
3596        mock_ctx.close().await;
3597    }
3598
3599    #[tokio::test(flavor = "multi_thread")]
3600    async fn test_lines_equal_length() {
3601        let initial_source = "\
3602@settings(experimentalFeatures = allow)
3603
3604sketch(on = XY) {
3605  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3606  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3607}
3608";
3609
3610        let program = Program::parse(initial_source).unwrap().0.unwrap();
3611
3612        let mut frontend = FrontendState::new();
3613
3614        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3615        let mock_ctx = ExecutorContext::new_mock(None).await;
3616        let version = Version(0);
3617
3618        frontend.hack_set_program(&ctx, program).await.unwrap();
3619        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3620        let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3621        let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3622
3623        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
3624            lines: vec![line1_id, line2_id],
3625        });
3626        let (src_delta, scene_delta) = frontend
3627            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3628            .await
3629            .unwrap();
3630        assert_eq!(
3631            src_delta.text.as_str(),
3632            "\
3633@settings(experimentalFeatures = allow)
3634
3635sketch(on = XY) {
3636  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3637  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3638  sketch2::equalLength([line1, line2])
3639}
3640"
3641        );
3642        assert_eq!(
3643            scene_delta.new_graph.objects.len(),
3644            8,
3645            "{:#?}",
3646            scene_delta.new_graph.objects
3647        );
3648
3649        ctx.close().await;
3650        mock_ctx.close().await;
3651    }
3652
3653    #[tokio::test(flavor = "multi_thread")]
3654    async fn test_lines_parallel() {
3655        let initial_source = "\
3656@settings(experimentalFeatures = allow)
3657
3658sketch(on = XY) {
3659  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3660  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3661}
3662";
3663
3664        let program = Program::parse(initial_source).unwrap().0.unwrap();
3665
3666        let mut frontend = FrontendState::new();
3667
3668        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3669        let mock_ctx = ExecutorContext::new_mock(None).await;
3670        let version = Version(0);
3671
3672        frontend.hack_set_program(&ctx, program).await.unwrap();
3673        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3674        let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3675        let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3676
3677        let constraint = Constraint::Parallel(Parallel {
3678            lines: vec![line1_id, line2_id],
3679        });
3680        let (src_delta, scene_delta) = frontend
3681            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3682            .await
3683            .unwrap();
3684        assert_eq!(
3685            src_delta.text.as_str(),
3686            "\
3687@settings(experimentalFeatures = allow)
3688
3689sketch(on = XY) {
3690  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3691  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3692  sketch2::parallel([line1, line2])
3693}
3694"
3695        );
3696        assert_eq!(
3697            scene_delta.new_graph.objects.len(),
3698            8,
3699            "{:#?}",
3700            scene_delta.new_graph.objects
3701        );
3702
3703        ctx.close().await;
3704        mock_ctx.close().await;
3705    }
3706
3707    #[tokio::test(flavor = "multi_thread")]
3708    async fn test_lines_perpendicular() {
3709        let initial_source = "\
3710@settings(experimentalFeatures = allow)
3711
3712sketch(on = XY) {
3713  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3714  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3715}
3716";
3717
3718        let program = Program::parse(initial_source).unwrap().0.unwrap();
3719
3720        let mut frontend = FrontendState::new();
3721
3722        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3723        let mock_ctx = ExecutorContext::new_mock(None).await;
3724        let version = Version(0);
3725
3726        frontend.hack_set_program(&ctx, program).await.unwrap();
3727        let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3728        let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3729        let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3730
3731        let constraint = Constraint::Perpendicular(Perpendicular {
3732            lines: vec![line1_id, line2_id],
3733        });
3734        let (src_delta, scene_delta) = frontend
3735            .add_constraint(&mock_ctx, version, sketch_id, constraint)
3736            .await
3737            .unwrap();
3738        assert_eq!(
3739            src_delta.text.as_str(),
3740            "\
3741@settings(experimentalFeatures = allow)
3742
3743sketch(on = XY) {
3744  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3745  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3746  sketch2::perpendicular([line1, line2])
3747}
3748"
3749        );
3750        assert_eq!(
3751            scene_delta.new_graph.objects.len(),
3752            8,
3753            "{:#?}",
3754            scene_delta.new_graph.objects
3755        );
3756
3757        ctx.close().await;
3758        mock_ctx.close().await;
3759    }
3760}