Skip to main content

kcl_lib/
frontend.rs

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