kcl_lib/
frontend.rs

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