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