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