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