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, 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 err.error.message().contains("websocket closed early") {
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: vec![ast::Node::no_src(ast::Identifier {
3501            name: "sketch2".to_owned(),
3502            digest: None,
3503        })],
3504        abs_path: false,
3505        digest: None,
3506    }
3507}
3508
3509// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
3510
3511/// Create an AST node for sketch2::coincident([expr1, expr2])
3512pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
3513    // Create array [expr1, expr2]
3514    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3515        elements: vec![expr1, expr2],
3516        digest: None,
3517        non_code_meta: Default::default(),
3518    })));
3519
3520    // Create sketch2::coincident([...])
3521    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3522        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
3523        unlabeled: Some(array_expr),
3524        arguments: Default::default(),
3525        digest: None,
3526        non_code_meta: Default::default(),
3527    })))
3528}
3529
3530/// Create an AST node for sketch2::line(start = [...], end = [...])
3531pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
3532    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3533        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
3534        unlabeled: None,
3535        arguments: vec![
3536            ast::LabeledArg {
3537                label: Some(ast::Identifier::new(LINE_START_PARAM)),
3538                arg: start_ast,
3539            },
3540            ast::LabeledArg {
3541                label: Some(ast::Identifier::new(LINE_END_PARAM)),
3542                arg: end_ast,
3543            },
3544        ],
3545        digest: None,
3546        non_code_meta: Default::default(),
3547    })))
3548}
3549
3550/// Create an AST node for sketch2::horizontal(line)
3551pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
3552    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3553        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
3554        unlabeled: Some(line_expr),
3555        arguments: Default::default(),
3556        digest: None,
3557        non_code_meta: Default::default(),
3558    })))
3559}
3560
3561/// Create an AST node for sketch2::vertical(line)
3562pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
3563    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3564        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
3565        unlabeled: Some(line_expr),
3566        arguments: Default::default(),
3567        digest: None,
3568        non_code_meta: Default::default(),
3569    })))
3570}
3571
3572/// Create a member expression like object.property (e.g., line1.end)
3573pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
3574    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
3575        object: object_expr,
3576        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
3577            name: ast::Node::no_src(ast::Identifier {
3578                name: property.to_string(),
3579                digest: None,
3580            }),
3581            path: Vec::new(),
3582            abs_path: false,
3583            digest: None,
3584        }))),
3585        computed: false,
3586        digest: None,
3587    })))
3588}
3589
3590/// Create an AST node for sketch2::equalLength([line1, line2])
3591pub(crate) fn create_equal_length_ast(line1_expr: ast::Expr, line2_expr: ast::Expr) -> ast::Expr {
3592    // Create array [line1, line2]
3593    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3594        elements: vec![line1_expr, line2_expr],
3595        digest: None,
3596        non_code_meta: Default::default(),
3597    })));
3598
3599    // Create sketch2::equalLength([...])
3600    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3601        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
3602        unlabeled: Some(array_expr),
3603        arguments: Default::default(),
3604        digest: None,
3605        non_code_meta: Default::default(),
3606    })))
3607}
3608
3609#[cfg(test)]
3610mod tests {
3611    use super::*;
3612    use crate::{
3613        engine::PlaneName,
3614        front::{Distance, Object, Plane, Sketch},
3615        frontend::sketch::Vertical,
3616        pretty::NumericSuffix,
3617    };
3618
3619    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
3620        for object in &scene_graph.objects {
3621            if let ObjectKind::Sketch(_) = &object.kind {
3622                return Some(object);
3623            }
3624        }
3625        None
3626    }
3627
3628    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
3629        for object in &scene_graph.objects {
3630            if let ObjectKind::Face(_) = &object.kind {
3631                return Some(object);
3632            }
3633        }
3634        None
3635    }
3636
3637    #[track_caller]
3638    fn expect_sketch(object: &Object) -> &Sketch {
3639        if let ObjectKind::Sketch(sketch) = &object.kind {
3640            sketch
3641        } else {
3642            panic!("Object is not a sketch: {:?}", object);
3643        }
3644    }
3645
3646    #[tokio::test(flavor = "multi_thread")]
3647    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
3648        let source = "\
3649@settings(experimentalFeatures = allow)
3650
3651sketch(on = XY) {
3652  line1 = sketch2::line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3653}
3654
3655bad = missing_name
3656";
3657        let program = Program::parse(source).unwrap().0.unwrap();
3658
3659        let mut frontend = FrontendState::new();
3660
3661        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3662        let mock_ctx = ExecutorContext::new_mock(None).await;
3663        let version = Version(0);
3664        let project_id = ProjectId(0);
3665        let file_id = FileId(0);
3666
3667        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
3668            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
3669        };
3670
3671        let sketch_id = frontend
3672            .scene_graph
3673            .objects
3674            .iter()
3675            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
3676            .expect("Expected sketch object from errored hack_set_program");
3677
3678        frontend
3679            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
3680            .await
3681            .unwrap();
3682
3683        ctx.close().await;
3684        mock_ctx.close().await;
3685    }
3686
3687    #[tokio::test(flavor = "multi_thread")]
3688    async fn test_new_sketch_add_point_edit_point() {
3689        let program = Program::empty();
3690
3691        let mut frontend = FrontendState::new();
3692        frontend.program = program;
3693
3694        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3695        let mock_ctx = ExecutorContext::new_mock(None).await;
3696        let version = Version(0);
3697
3698        let sketch_args = SketchCtor {
3699            on: PlaneName::Xy.to_string(),
3700        };
3701        let (_src_delta, scene_delta, sketch_id) = frontend
3702            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3703            .await
3704            .unwrap();
3705        assert_eq!(sketch_id, ObjectId(1));
3706        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3707        let sketch_object = &scene_delta.new_graph.objects[1];
3708        assert_eq!(sketch_object.id, ObjectId(1));
3709        assert_eq!(
3710            sketch_object.kind,
3711            ObjectKind::Sketch(Sketch {
3712                args: SketchCtor {
3713                    on: PlaneName::Xy.to_string()
3714                },
3715                plane: ObjectId(0),
3716                segments: vec![],
3717                constraints: vec![],
3718            })
3719        );
3720        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3721
3722        let point_ctor = PointCtor {
3723            position: Point2d {
3724                x: Expr::Number(Number {
3725                    value: 1.0,
3726                    units: NumericSuffix::Inch,
3727                }),
3728                y: Expr::Number(Number {
3729                    value: 2.0,
3730                    units: NumericSuffix::Inch,
3731                }),
3732            },
3733        };
3734        let segment = SegmentCtor::Point(point_ctor);
3735        let (src_delta, scene_delta) = frontend
3736            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3737            .await
3738            .unwrap();
3739        assert_eq!(
3740            src_delta.text.as_str(),
3741            "@settings(experimentalFeatures = allow)
3742
3743sketch(on = XY) {
3744  sketch2::point(at = [1in, 2in])
3745}
3746"
3747        );
3748        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
3749        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3750        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3751            assert_eq!(scene_object.id.0, i);
3752        }
3753
3754        let point_id = *scene_delta.new_objects.last().unwrap();
3755
3756        let point_ctor = PointCtor {
3757            position: Point2d {
3758                x: Expr::Number(Number {
3759                    value: 3.0,
3760                    units: NumericSuffix::Inch,
3761                }),
3762                y: Expr::Number(Number {
3763                    value: 4.0,
3764                    units: NumericSuffix::Inch,
3765                }),
3766            },
3767        };
3768        let segments = vec![ExistingSegmentCtor {
3769            id: point_id,
3770            ctor: SegmentCtor::Point(point_ctor),
3771        }];
3772        let (src_delta, scene_delta) = frontend
3773            .edit_segments(&mock_ctx, version, sketch_id, segments)
3774            .await
3775            .unwrap();
3776        assert_eq!(
3777            src_delta.text.as_str(),
3778            "@settings(experimentalFeatures = allow)
3779
3780sketch(on = XY) {
3781  sketch2::point(at = [3in, 4in])
3782}
3783"
3784        );
3785        assert_eq!(scene_delta.new_objects, vec![]);
3786        assert_eq!(scene_delta.new_graph.objects.len(), 3);
3787
3788        ctx.close().await;
3789        mock_ctx.close().await;
3790    }
3791
3792    #[tokio::test(flavor = "multi_thread")]
3793    async fn test_new_sketch_add_line_edit_line() {
3794        let program = Program::empty();
3795
3796        let mut frontend = FrontendState::new();
3797        frontend.program = program;
3798
3799        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3800        let mock_ctx = ExecutorContext::new_mock(None).await;
3801        let version = Version(0);
3802
3803        let sketch_args = SketchCtor {
3804            on: PlaneName::Xy.to_string(),
3805        };
3806        let (_src_delta, scene_delta, sketch_id) = frontend
3807            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3808            .await
3809            .unwrap();
3810        assert_eq!(sketch_id, ObjectId(1));
3811        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3812        let sketch_object = &scene_delta.new_graph.objects[1];
3813        assert_eq!(sketch_object.id, ObjectId(1));
3814        assert_eq!(
3815            sketch_object.kind,
3816            ObjectKind::Sketch(Sketch {
3817                args: SketchCtor {
3818                    on: PlaneName::Xy.to_string()
3819                },
3820                plane: ObjectId(0),
3821                segments: vec![],
3822                constraints: vec![],
3823            })
3824        );
3825        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3826
3827        let line_ctor = LineCtor {
3828            start: Point2d {
3829                x: Expr::Number(Number {
3830                    value: 0.0,
3831                    units: NumericSuffix::Mm,
3832                }),
3833                y: Expr::Number(Number {
3834                    value: 0.0,
3835                    units: NumericSuffix::Mm,
3836                }),
3837            },
3838            end: Point2d {
3839                x: Expr::Number(Number {
3840                    value: 10.0,
3841                    units: NumericSuffix::Mm,
3842                }),
3843                y: Expr::Number(Number {
3844                    value: 10.0,
3845                    units: NumericSuffix::Mm,
3846                }),
3847            },
3848            construction: None,
3849        };
3850        let segment = SegmentCtor::Line(line_ctor);
3851        let (src_delta, scene_delta) = frontend
3852            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3853            .await
3854            .unwrap();
3855        assert_eq!(
3856            src_delta.text.as_str(),
3857            "@settings(experimentalFeatures = allow)
3858
3859sketch(on = XY) {
3860  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3861}
3862"
3863        );
3864        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3865        assert_eq!(scene_delta.new_graph.objects.len(), 5);
3866        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3867            assert_eq!(scene_object.id.0, i);
3868        }
3869
3870        // The new objects are the end points and then the line.
3871        let line = *scene_delta.new_objects.last().unwrap();
3872
3873        let line_ctor = LineCtor {
3874            start: Point2d {
3875                x: Expr::Number(Number {
3876                    value: 1.0,
3877                    units: NumericSuffix::Mm,
3878                }),
3879                y: Expr::Number(Number {
3880                    value: 2.0,
3881                    units: NumericSuffix::Mm,
3882                }),
3883            },
3884            end: Point2d {
3885                x: Expr::Number(Number {
3886                    value: 13.0,
3887                    units: NumericSuffix::Mm,
3888                }),
3889                y: Expr::Number(Number {
3890                    value: 14.0,
3891                    units: NumericSuffix::Mm,
3892                }),
3893            },
3894            construction: None,
3895        };
3896        let segments = vec![ExistingSegmentCtor {
3897            id: line,
3898            ctor: SegmentCtor::Line(line_ctor),
3899        }];
3900        let (src_delta, scene_delta) = frontend
3901            .edit_segments(&mock_ctx, version, sketch_id, segments)
3902            .await
3903            .unwrap();
3904        assert_eq!(
3905            src_delta.text.as_str(),
3906            "@settings(experimentalFeatures = allow)
3907
3908sketch(on = XY) {
3909  sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
3910}
3911"
3912        );
3913        assert_eq!(scene_delta.new_objects, vec![]);
3914        assert_eq!(scene_delta.new_graph.objects.len(), 5);
3915
3916        ctx.close().await;
3917        mock_ctx.close().await;
3918    }
3919
3920    #[tokio::test(flavor = "multi_thread")]
3921    async fn test_new_sketch_add_arc_edit_arc() {
3922        let program = Program::empty();
3923
3924        let mut frontend = FrontendState::new();
3925        frontend.program = program;
3926
3927        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3928        let mock_ctx = ExecutorContext::new_mock(None).await;
3929        let version = Version(0);
3930
3931        let sketch_args = SketchCtor {
3932            on: PlaneName::Xy.to_string(),
3933        };
3934        let (_src_delta, scene_delta, sketch_id) = frontend
3935            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3936            .await
3937            .unwrap();
3938        assert_eq!(sketch_id, ObjectId(1));
3939        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3940        let sketch_object = &scene_delta.new_graph.objects[1];
3941        assert_eq!(sketch_object.id, ObjectId(1));
3942        assert_eq!(
3943            sketch_object.kind,
3944            ObjectKind::Sketch(Sketch {
3945                args: SketchCtor {
3946                    on: PlaneName::Xy.to_string(),
3947                },
3948                plane: ObjectId(0),
3949                segments: vec![],
3950                constraints: vec![],
3951            })
3952        );
3953        assert_eq!(scene_delta.new_graph.objects.len(), 2);
3954
3955        let arc_ctor = ArcCtor {
3956            start: Point2d {
3957                x: Expr::Var(Number {
3958                    value: 0.0,
3959                    units: NumericSuffix::Mm,
3960                }),
3961                y: Expr::Var(Number {
3962                    value: 0.0,
3963                    units: NumericSuffix::Mm,
3964                }),
3965            },
3966            end: Point2d {
3967                x: Expr::Var(Number {
3968                    value: 10.0,
3969                    units: NumericSuffix::Mm,
3970                }),
3971                y: Expr::Var(Number {
3972                    value: 10.0,
3973                    units: NumericSuffix::Mm,
3974                }),
3975            },
3976            center: Point2d {
3977                x: Expr::Var(Number {
3978                    value: 10.0,
3979                    units: NumericSuffix::Mm,
3980                }),
3981                y: Expr::Var(Number {
3982                    value: 0.0,
3983                    units: NumericSuffix::Mm,
3984                }),
3985            },
3986            construction: None,
3987        };
3988        let segment = SegmentCtor::Arc(arc_ctor);
3989        let (src_delta, scene_delta) = frontend
3990            .add_segment(&mock_ctx, version, sketch_id, segment, None)
3991            .await
3992            .unwrap();
3993        assert_eq!(
3994            src_delta.text.as_str(),
3995            "@settings(experimentalFeatures = allow)
3996
3997sketch(on = XY) {
3998  sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
3999}
4000"
4001        );
4002        assert_eq!(
4003            scene_delta.new_objects,
4004            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
4005        );
4006        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4007            assert_eq!(scene_object.id.0, i);
4008        }
4009        assert_eq!(scene_delta.new_graph.objects.len(), 6);
4010
4011        // The new objects are the end points, the center, and then the arc.
4012        let arc = *scene_delta.new_objects.last().unwrap();
4013
4014        let arc_ctor = ArcCtor {
4015            start: Point2d {
4016                x: Expr::Var(Number {
4017                    value: 1.0,
4018                    units: NumericSuffix::Mm,
4019                }),
4020                y: Expr::Var(Number {
4021                    value: 2.0,
4022                    units: NumericSuffix::Mm,
4023                }),
4024            },
4025            end: Point2d {
4026                x: Expr::Var(Number {
4027                    value: 13.0,
4028                    units: NumericSuffix::Mm,
4029                }),
4030                y: Expr::Var(Number {
4031                    value: 14.0,
4032                    units: NumericSuffix::Mm,
4033                }),
4034            },
4035            center: Point2d {
4036                x: Expr::Var(Number {
4037                    value: 13.0,
4038                    units: NumericSuffix::Mm,
4039                }),
4040                y: Expr::Var(Number {
4041                    value: 2.0,
4042                    units: NumericSuffix::Mm,
4043                }),
4044            },
4045            construction: None,
4046        };
4047        let segments = vec![ExistingSegmentCtor {
4048            id: arc,
4049            ctor: SegmentCtor::Arc(arc_ctor),
4050        }];
4051        let (src_delta, scene_delta) = frontend
4052            .edit_segments(&mock_ctx, version, sketch_id, segments)
4053            .await
4054            .unwrap();
4055        assert_eq!(
4056            src_delta.text.as_str(),
4057            "@settings(experimentalFeatures = allow)
4058
4059sketch(on = XY) {
4060  sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
4061}
4062"
4063        );
4064        assert_eq!(scene_delta.new_objects, vec![]);
4065        assert_eq!(scene_delta.new_graph.objects.len(), 6);
4066
4067        ctx.close().await;
4068        mock_ctx.close().await;
4069    }
4070
4071    #[tokio::test(flavor = "multi_thread")]
4072    async fn test_add_line_when_sketch_block_uses_variable() {
4073        let initial_source = "@settings(experimentalFeatures = allow)
4074
4075s = sketch(on = XY) {}
4076";
4077
4078        let program = Program::parse(initial_source).unwrap().0.unwrap();
4079
4080        let mut frontend = FrontendState::new();
4081
4082        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4083        let mock_ctx = ExecutorContext::new_mock(None).await;
4084        let version = Version(0);
4085
4086        frontend.hack_set_program(&ctx, program).await.unwrap();
4087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4088        let sketch_id = sketch_object.id;
4089
4090        let line_ctor = LineCtor {
4091            start: Point2d {
4092                x: Expr::Number(Number {
4093                    value: 0.0,
4094                    units: NumericSuffix::Mm,
4095                }),
4096                y: Expr::Number(Number {
4097                    value: 0.0,
4098                    units: NumericSuffix::Mm,
4099                }),
4100            },
4101            end: Point2d {
4102                x: Expr::Number(Number {
4103                    value: 10.0,
4104                    units: NumericSuffix::Mm,
4105                }),
4106                y: Expr::Number(Number {
4107                    value: 10.0,
4108                    units: NumericSuffix::Mm,
4109                }),
4110            },
4111            construction: None,
4112        };
4113        let segment = SegmentCtor::Line(line_ctor);
4114        let (src_delta, scene_delta) = frontend
4115            .add_segment(&mock_ctx, version, sketch_id, segment, None)
4116            .await
4117            .unwrap();
4118        assert_eq!(
4119            src_delta.text.as_str(),
4120            "@settings(experimentalFeatures = allow)
4121
4122s = sketch(on = XY) {
4123  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
4124}
4125"
4126        );
4127        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
4128        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4129
4130        ctx.close().await;
4131        mock_ctx.close().await;
4132    }
4133
4134    #[tokio::test(flavor = "multi_thread")]
4135    async fn test_new_sketch_add_line_delete_sketch() {
4136        let program = Program::empty();
4137
4138        let mut frontend = FrontendState::new();
4139        frontend.program = program;
4140
4141        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4142        let mock_ctx = ExecutorContext::new_mock(None).await;
4143        let version = Version(0);
4144
4145        let sketch_args = SketchCtor {
4146            on: PlaneName::Xy.to_string(),
4147        };
4148        let (_src_delta, scene_delta, sketch_id) = frontend
4149            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4150            .await
4151            .unwrap();
4152        assert_eq!(sketch_id, ObjectId(1));
4153        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4154        let sketch_object = &scene_delta.new_graph.objects[1];
4155        assert_eq!(sketch_object.id, ObjectId(1));
4156        assert_eq!(
4157            sketch_object.kind,
4158            ObjectKind::Sketch(Sketch {
4159                args: SketchCtor {
4160                    on: PlaneName::Xy.to_string()
4161                },
4162                plane: ObjectId(0),
4163                segments: vec![],
4164                constraints: vec![],
4165            })
4166        );
4167        assert_eq!(scene_delta.new_graph.objects.len(), 2);
4168
4169        let line_ctor = LineCtor {
4170            start: Point2d {
4171                x: Expr::Number(Number {
4172                    value: 0.0,
4173                    units: NumericSuffix::Mm,
4174                }),
4175                y: Expr::Number(Number {
4176                    value: 0.0,
4177                    units: NumericSuffix::Mm,
4178                }),
4179            },
4180            end: Point2d {
4181                x: Expr::Number(Number {
4182                    value: 10.0,
4183                    units: NumericSuffix::Mm,
4184                }),
4185                y: Expr::Number(Number {
4186                    value: 10.0,
4187                    units: NumericSuffix::Mm,
4188                }),
4189            },
4190            construction: None,
4191        };
4192        let segment = SegmentCtor::Line(line_ctor);
4193        let (src_delta, scene_delta) = frontend
4194            .add_segment(&mock_ctx, version, sketch_id, segment, None)
4195            .await
4196            .unwrap();
4197        assert_eq!(
4198            src_delta.text.as_str(),
4199            "@settings(experimentalFeatures = allow)
4200
4201sketch(on = XY) {
4202  sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
4203}
4204"
4205        );
4206        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4207
4208        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4209        assert_eq!(
4210            src_delta.text.as_str(),
4211            "@settings(experimentalFeatures = allow)
4212"
4213        );
4214        assert_eq!(scene_delta.new_graph.objects.len(), 0);
4215
4216        ctx.close().await;
4217        mock_ctx.close().await;
4218    }
4219
4220    #[tokio::test(flavor = "multi_thread")]
4221    async fn test_delete_sketch_when_sketch_block_uses_variable() {
4222        let initial_source = "@settings(experimentalFeatures = allow)
4223
4224s = sketch(on = XY) {}
4225";
4226
4227        let program = Program::parse(initial_source).unwrap().0.unwrap();
4228
4229        let mut frontend = FrontendState::new();
4230
4231        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4232        let mock_ctx = ExecutorContext::new_mock(None).await;
4233        let version = Version(0);
4234
4235        frontend.hack_set_program(&ctx, program).await.unwrap();
4236        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4237        let sketch_id = sketch_object.id;
4238
4239        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4240        assert_eq!(
4241            src_delta.text.as_str(),
4242            "@settings(experimentalFeatures = allow)
4243"
4244        );
4245        assert_eq!(scene_delta.new_graph.objects.len(), 0);
4246
4247        ctx.close().await;
4248        mock_ctx.close().await;
4249    }
4250
4251    #[tokio::test(flavor = "multi_thread")]
4252    async fn test_edit_line_when_editing_its_start_point() {
4253        let initial_source = "\
4254@settings(experimentalFeatures = allow)
4255
4256sketch(on = XY) {
4257  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4258}
4259";
4260
4261        let program = Program::parse(initial_source).unwrap().0.unwrap();
4262
4263        let mut frontend = FrontendState::new();
4264
4265        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4266        let mock_ctx = ExecutorContext::new_mock(None).await;
4267        let version = Version(0);
4268
4269        frontend.hack_set_program(&ctx, program).await.unwrap();
4270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4271        let sketch_id = sketch_object.id;
4272        let sketch = expect_sketch(sketch_object);
4273
4274        let point_id = *sketch.segments.first().unwrap();
4275
4276        let point_ctor = PointCtor {
4277            position: Point2d {
4278                x: Expr::Var(Number {
4279                    value: 5.0,
4280                    units: NumericSuffix::Inch,
4281                }),
4282                y: Expr::Var(Number {
4283                    value: 6.0,
4284                    units: NumericSuffix::Inch,
4285                }),
4286            },
4287        };
4288        let segments = vec![ExistingSegmentCtor {
4289            id: point_id,
4290            ctor: SegmentCtor::Point(point_ctor),
4291        }];
4292        let (src_delta, scene_delta) = frontend
4293            .edit_segments(&mock_ctx, version, sketch_id, segments)
4294            .await
4295            .unwrap();
4296        assert_eq!(
4297            src_delta.text.as_str(),
4298            "\
4299@settings(experimentalFeatures = allow)
4300
4301sketch(on = XY) {
4302  sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
4303}
4304"
4305        );
4306        assert_eq!(scene_delta.new_objects, vec![]);
4307        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4308
4309        ctx.close().await;
4310        mock_ctx.close().await;
4311    }
4312
4313    #[tokio::test(flavor = "multi_thread")]
4314    async fn test_edit_line_when_editing_its_end_point() {
4315        let initial_source = "\
4316@settings(experimentalFeatures = allow)
4317
4318sketch(on = XY) {
4319  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4320}
4321";
4322
4323        let program = Program::parse(initial_source).unwrap().0.unwrap();
4324
4325        let mut frontend = FrontendState::new();
4326
4327        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4328        let mock_ctx = ExecutorContext::new_mock(None).await;
4329        let version = Version(0);
4330
4331        frontend.hack_set_program(&ctx, program).await.unwrap();
4332        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4333        let sketch_id = sketch_object.id;
4334        let sketch = expect_sketch(sketch_object);
4335        let point_id = *sketch.segments.get(1).unwrap();
4336
4337        let point_ctor = PointCtor {
4338            position: Point2d {
4339                x: Expr::Var(Number {
4340                    value: 5.0,
4341                    units: NumericSuffix::Inch,
4342                }),
4343                y: Expr::Var(Number {
4344                    value: 6.0,
4345                    units: NumericSuffix::Inch,
4346                }),
4347            },
4348        };
4349        let segments = vec![ExistingSegmentCtor {
4350            id: point_id,
4351            ctor: SegmentCtor::Point(point_ctor),
4352        }];
4353        let (src_delta, scene_delta) = frontend
4354            .edit_segments(&mock_ctx, version, sketch_id, segments)
4355            .await
4356            .unwrap();
4357        assert_eq!(
4358            src_delta.text.as_str(),
4359            "\
4360@settings(experimentalFeatures = allow)
4361
4362sketch(on = XY) {
4363  sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
4364}
4365"
4366        );
4367        assert_eq!(scene_delta.new_objects, vec![]);
4368        assert_eq!(
4369            scene_delta.new_graph.objects.len(),
4370            5,
4371            "{:#?}",
4372            scene_delta.new_graph.objects
4373        );
4374
4375        ctx.close().await;
4376        mock_ctx.close().await;
4377    }
4378
4379    #[tokio::test(flavor = "multi_thread")]
4380    async fn test_edit_line_with_coincident_feedback() {
4381        let initial_source = "\
4382@settings(experimentalFeatures = allow)
4383
4384sketch(on = XY) {
4385  line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
4386  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4387  line1.start.at[0] == 0
4388  line1.start.at[1] == 0
4389  sketch2::coincident([line1.end, line2.start])
4390  sketch2::equalLength([line1, line2])
4391}
4392";
4393
4394        let program = Program::parse(initial_source).unwrap().0.unwrap();
4395
4396        let mut frontend = FrontendState::new();
4397
4398        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4399        let mock_ctx = ExecutorContext::new_mock(None).await;
4400        let version = Version(0);
4401
4402        frontend.hack_set_program(&ctx, program).await.unwrap();
4403        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4404        let sketch_id = sketch_object.id;
4405        let sketch = expect_sketch(sketch_object);
4406        let line2_end_id = *sketch.segments.get(4).unwrap();
4407
4408        let segments = vec![ExistingSegmentCtor {
4409            id: line2_end_id,
4410            ctor: SegmentCtor::Point(PointCtor {
4411                position: Point2d {
4412                    x: Expr::Var(Number {
4413                        value: 9.0,
4414                        units: NumericSuffix::None,
4415                    }),
4416                    y: Expr::Var(Number {
4417                        value: 10.0,
4418                        units: NumericSuffix::None,
4419                    }),
4420                },
4421            }),
4422        }];
4423        let (src_delta, scene_delta) = frontend
4424            .edit_segments(&mock_ctx, version, sketch_id, segments)
4425            .await
4426            .unwrap();
4427        assert_eq!(
4428            src_delta.text.as_str(),
4429            "\
4430@settings(experimentalFeatures = allow)
4431
4432sketch(on = XY) {
4433  line1 = sketch2::line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
4434  line2 = sketch2::line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
4435line1.start.at[0] == 0
4436line1.start.at[1] == 0
4437  sketch2::coincident([line1.end, line2.start])
4438  sketch2::equalLength([line1, line2])
4439}
4440"
4441        );
4442        assert_eq!(
4443            scene_delta.new_graph.objects.len(),
4444            10,
4445            "{:#?}",
4446            scene_delta.new_graph.objects
4447        );
4448
4449        ctx.close().await;
4450        mock_ctx.close().await;
4451    }
4452
4453    #[tokio::test(flavor = "multi_thread")]
4454    async fn test_delete_point_without_var() {
4455        let initial_source = "\
4456@settings(experimentalFeatures = allow)
4457
4458sketch(on = XY) {
4459  sketch2::point(at = [var 1, var 2])
4460  sketch2::point(at = [var 3, var 4])
4461  sketch2::point(at = [var 5, var 6])
4462}
4463";
4464
4465        let program = Program::parse(initial_source).unwrap().0.unwrap();
4466
4467        let mut frontend = FrontendState::new();
4468
4469        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4470        let mock_ctx = ExecutorContext::new_mock(None).await;
4471        let version = Version(0);
4472
4473        frontend.hack_set_program(&ctx, program).await.unwrap();
4474        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4475        let sketch_id = sketch_object.id;
4476        let sketch = expect_sketch(sketch_object);
4477
4478        let point_id = *sketch.segments.get(1).unwrap();
4479
4480        let (src_delta, scene_delta) = frontend
4481            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4482            .await
4483            .unwrap();
4484        assert_eq!(
4485            src_delta.text.as_str(),
4486            "\
4487@settings(experimentalFeatures = allow)
4488
4489sketch(on = XY) {
4490  sketch2::point(at = [var 1mm, var 2mm])
4491  sketch2::point(at = [var 5mm, var 6mm])
4492}
4493"
4494        );
4495        assert_eq!(scene_delta.new_objects, vec![]);
4496        assert_eq!(scene_delta.new_graph.objects.len(), 4);
4497
4498        ctx.close().await;
4499        mock_ctx.close().await;
4500    }
4501
4502    #[tokio::test(flavor = "multi_thread")]
4503    async fn test_delete_point_with_var() {
4504        let initial_source = "\
4505@settings(experimentalFeatures = allow)
4506
4507sketch(on = XY) {
4508  sketch2::point(at = [var 1, var 2])
4509  point1 = sketch2::point(at = [var 3, var 4])
4510  sketch2::point(at = [var 5, var 6])
4511}
4512";
4513
4514        let program = Program::parse(initial_source).unwrap().0.unwrap();
4515
4516        let mut frontend = FrontendState::new();
4517
4518        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4519        let mock_ctx = ExecutorContext::new_mock(None).await;
4520        let version = Version(0);
4521
4522        frontend.hack_set_program(&ctx, program).await.unwrap();
4523        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4524        let sketch_id = sketch_object.id;
4525        let sketch = expect_sketch(sketch_object);
4526
4527        let point_id = *sketch.segments.get(1).unwrap();
4528
4529        let (src_delta, scene_delta) = frontend
4530            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4531            .await
4532            .unwrap();
4533        assert_eq!(
4534            src_delta.text.as_str(),
4535            "\
4536@settings(experimentalFeatures = allow)
4537
4538sketch(on = XY) {
4539  sketch2::point(at = [var 1mm, var 2mm])
4540  sketch2::point(at = [var 5mm, var 6mm])
4541}
4542"
4543        );
4544        assert_eq!(scene_delta.new_objects, vec![]);
4545        assert_eq!(scene_delta.new_graph.objects.len(), 4);
4546
4547        ctx.close().await;
4548        mock_ctx.close().await;
4549    }
4550
4551    #[tokio::test(flavor = "multi_thread")]
4552    async fn test_delete_multiple_points() {
4553        let initial_source = "\
4554@settings(experimentalFeatures = allow)
4555
4556sketch(on = XY) {
4557  sketch2::point(at = [var 1, var 2])
4558  point1 = sketch2::point(at = [var 3, var 4])
4559  sketch2::point(at = [var 5, var 6])
4560}
4561";
4562
4563        let program = Program::parse(initial_source).unwrap().0.unwrap();
4564
4565        let mut frontend = FrontendState::new();
4566
4567        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4568        let mock_ctx = ExecutorContext::new_mock(None).await;
4569        let version = Version(0);
4570
4571        frontend.hack_set_program(&ctx, program).await.unwrap();
4572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4573        let sketch_id = sketch_object.id;
4574
4575        let sketch = expect_sketch(sketch_object);
4576
4577        let point1_id = *sketch.segments.first().unwrap();
4578        let point2_id = *sketch.segments.get(1).unwrap();
4579
4580        let (src_delta, scene_delta) = frontend
4581            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
4582            .await
4583            .unwrap();
4584        assert_eq!(
4585            src_delta.text.as_str(),
4586            "\
4587@settings(experimentalFeatures = allow)
4588
4589sketch(on = XY) {
4590  sketch2::point(at = [var 5mm, var 6mm])
4591}
4592"
4593        );
4594        assert_eq!(scene_delta.new_objects, vec![]);
4595        assert_eq!(scene_delta.new_graph.objects.len(), 3);
4596
4597        ctx.close().await;
4598        mock_ctx.close().await;
4599    }
4600
4601    #[tokio::test(flavor = "multi_thread")]
4602    async fn test_delete_coincident_constraint() {
4603        let initial_source = "\
4604@settings(experimentalFeatures = allow)
4605
4606sketch(on = XY) {
4607  point1 = sketch2::point(at = [var 1, var 2])
4608  point2 = sketch2::point(at = [var 3, var 4])
4609  sketch2::coincident([point1, point2])
4610  sketch2::point(at = [var 5, var 6])
4611}
4612";
4613
4614        let program = Program::parse(initial_source).unwrap().0.unwrap();
4615
4616        let mut frontend = FrontendState::new();
4617
4618        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4619        let mock_ctx = ExecutorContext::new_mock(None).await;
4620        let version = Version(0);
4621
4622        frontend.hack_set_program(&ctx, program).await.unwrap();
4623        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4624        let sketch_id = sketch_object.id;
4625        let sketch = expect_sketch(sketch_object);
4626
4627        let coincident_id = *sketch.constraints.first().unwrap();
4628
4629        let (src_delta, scene_delta) = frontend
4630            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4631            .await
4632            .unwrap();
4633        assert_eq!(
4634            src_delta.text.as_str(),
4635            "\
4636@settings(experimentalFeatures = allow)
4637
4638sketch(on = XY) {
4639  point1 = sketch2::point(at = [var 1mm, var 2mm])
4640  point2 = sketch2::point(at = [var 3mm, var 4mm])
4641  sketch2::point(at = [var 5mm, var 6mm])
4642}
4643"
4644        );
4645        assert_eq!(scene_delta.new_objects, vec![]);
4646        assert_eq!(scene_delta.new_graph.objects.len(), 5);
4647
4648        ctx.close().await;
4649        mock_ctx.close().await;
4650    }
4651
4652    #[tokio::test(flavor = "multi_thread")]
4653    async fn test_delete_line_cascades_to_coincident_constraint() {
4654        let initial_source = "\
4655@settings(experimentalFeatures = allow)
4656
4657sketch(on = XY) {
4658  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4659  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4660  sketch2::coincident([line1.end, line2.start])
4661}
4662";
4663
4664        let program = Program::parse(initial_source).unwrap().0.unwrap();
4665
4666        let mut frontend = FrontendState::new();
4667
4668        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4669        let mock_ctx = ExecutorContext::new_mock(None).await;
4670        let version = Version(0);
4671
4672        frontend.hack_set_program(&ctx, program).await.unwrap();
4673        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4674        let sketch_id = sketch_object.id;
4675        let sketch = expect_sketch(sketch_object);
4676        let line_id = *sketch.segments.get(5).unwrap();
4677
4678        let (src_delta, scene_delta) = frontend
4679            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4680            .await
4681            .unwrap();
4682        assert_eq!(
4683            src_delta.text.as_str(),
4684            "\
4685@settings(experimentalFeatures = allow)
4686
4687sketch(on = XY) {
4688  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4689}
4690"
4691        );
4692        assert_eq!(
4693            scene_delta.new_graph.objects.len(),
4694            5,
4695            "{:#?}",
4696            scene_delta.new_graph.objects
4697        );
4698
4699        ctx.close().await;
4700        mock_ctx.close().await;
4701    }
4702
4703    #[tokio::test(flavor = "multi_thread")]
4704    async fn test_delete_line_cascades_to_distance_constraint() {
4705        let initial_source = "\
4706@settings(experimentalFeatures = allow)
4707
4708sketch(on = XY) {
4709  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4710  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4711  sketch2::distance([line1.end, line2.start]) == 10mm
4712}
4713";
4714
4715        let program = Program::parse(initial_source).unwrap().0.unwrap();
4716
4717        let mut frontend = FrontendState::new();
4718
4719        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4720        let mock_ctx = ExecutorContext::new_mock(None).await;
4721        let version = Version(0);
4722
4723        frontend.hack_set_program(&ctx, program).await.unwrap();
4724        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4725        let sketch_id = sketch_object.id;
4726        let sketch = expect_sketch(sketch_object);
4727        let line_id = *sketch.segments.get(5).unwrap();
4728
4729        let (src_delta, scene_delta) = frontend
4730            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4731            .await
4732            .unwrap();
4733        assert_eq!(
4734            src_delta.text.as_str(),
4735            "\
4736@settings(experimentalFeatures = allow)
4737
4738sketch(on = XY) {
4739  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4740}
4741"
4742        );
4743        assert_eq!(
4744            scene_delta.new_graph.objects.len(),
4745            5,
4746            "{:#?}",
4747            scene_delta.new_graph.objects
4748        );
4749
4750        ctx.close().await;
4751        mock_ctx.close().await;
4752    }
4753
4754    #[tokio::test(flavor = "multi_thread")]
4755    async fn test_delete_line_line_coincident_constraint() {
4756        let initial_source = "\
4757@settings(experimentalFeatures = allow)
4758
4759sketch(on = XY) {
4760  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4761  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4762  sketch2::coincident([line1, line2])
4763}
4764";
4765
4766        let program = Program::parse(initial_source).unwrap().0.unwrap();
4767
4768        let mut frontend = FrontendState::new();
4769
4770        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4771        let mock_ctx = ExecutorContext::new_mock(None).await;
4772        let version = Version(0);
4773
4774        frontend.hack_set_program(&ctx, program).await.unwrap();
4775        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4776        let sketch_id = sketch_object.id;
4777        let sketch = expect_sketch(sketch_object);
4778
4779        let coincident_id = *sketch.constraints.first().unwrap();
4780
4781        let (src_delta, scene_delta) = frontend
4782            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4783            .await
4784            .unwrap();
4785        assert_eq!(
4786            src_delta.text.as_str(),
4787            "\
4788@settings(experimentalFeatures = allow)
4789
4790sketch(on = XY) {
4791  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4792  line2 = sketch2::line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
4793}
4794"
4795        );
4796        assert_eq!(scene_delta.new_objects, vec![]);
4797        assert_eq!(scene_delta.new_graph.objects.len(), 8);
4798
4799        ctx.close().await;
4800        mock_ctx.close().await;
4801    }
4802
4803    #[tokio::test(flavor = "multi_thread")]
4804    async fn test_two_points_coincident() {
4805        let initial_source = "\
4806@settings(experimentalFeatures = allow)
4807
4808sketch(on = XY) {
4809  point1 = sketch2::point(at = [var 1, var 2])
4810  sketch2::point(at = [3, 4])
4811}
4812";
4813
4814        let program = Program::parse(initial_source).unwrap().0.unwrap();
4815
4816        let mut frontend = FrontendState::new();
4817
4818        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4819        let mock_ctx = ExecutorContext::new_mock(None).await;
4820        let version = Version(0);
4821
4822        frontend.hack_set_program(&ctx, program).await.unwrap();
4823        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4824        let sketch_id = sketch_object.id;
4825        let sketch = expect_sketch(sketch_object);
4826        let point0_id = *sketch.segments.first().unwrap();
4827        let point1_id = *sketch.segments.get(1).unwrap();
4828
4829        let constraint = Constraint::Coincident(Coincident {
4830            segments: vec![point0_id, point1_id],
4831        });
4832        let (src_delta, scene_delta) = frontend
4833            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4834            .await
4835            .unwrap();
4836        assert_eq!(
4837            src_delta.text.as_str(),
4838            "\
4839@settings(experimentalFeatures = allow)
4840
4841sketch(on = XY) {
4842  point1 = sketch2::point(at = [var 1, var 2])
4843  point2 = sketch2::point(at = [3, 4])
4844  sketch2::coincident([point1, point2])
4845}
4846"
4847        );
4848        assert_eq!(
4849            scene_delta.new_graph.objects.len(),
4850            5,
4851            "{:#?}",
4852            scene_delta.new_graph.objects
4853        );
4854
4855        ctx.close().await;
4856        mock_ctx.close().await;
4857    }
4858
4859    #[tokio::test(flavor = "multi_thread")]
4860    async fn test_coincident_of_line_end_points() {
4861        let initial_source = "\
4862@settings(experimentalFeatures = allow)
4863
4864sketch(on = XY) {
4865  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4866  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4867}
4868";
4869
4870        let program = Program::parse(initial_source).unwrap().0.unwrap();
4871
4872        let mut frontend = FrontendState::new();
4873
4874        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4875        let mock_ctx = ExecutorContext::new_mock(None).await;
4876        let version = Version(0);
4877
4878        frontend.hack_set_program(&ctx, program).await.unwrap();
4879        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4880        let sketch_id = sketch_object.id;
4881        let sketch = expect_sketch(sketch_object);
4882        let point0_id = *sketch.segments.get(1).unwrap();
4883        let point1_id = *sketch.segments.get(3).unwrap();
4884
4885        let constraint = Constraint::Coincident(Coincident {
4886            segments: vec![point0_id, point1_id],
4887        });
4888        let (src_delta, scene_delta) = frontend
4889            .add_constraint(&mock_ctx, version, sketch_id, constraint)
4890            .await
4891            .unwrap();
4892        assert_eq!(
4893            src_delta.text.as_str(),
4894            "\
4895@settings(experimentalFeatures = allow)
4896
4897sketch(on = XY) {
4898  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4899  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4900  sketch2::coincident([line1.end, line2.start])
4901}
4902"
4903        );
4904        assert_eq!(
4905            scene_delta.new_graph.objects.len(),
4906            9,
4907            "{:#?}",
4908            scene_delta.new_graph.objects
4909        );
4910
4911        ctx.close().await;
4912        mock_ctx.close().await;
4913    }
4914
4915    #[tokio::test(flavor = "multi_thread")]
4916    async fn test_invalid_coincident_arc_and_line_preserves_state() {
4917        // Test that attempting an invalid coincident constraint (arc and line)
4918        // doesn't corrupt the state, allowing subsequent operations to work.
4919        // This test verifies the transactional fix in add_constraint that prevents
4920        // state corruption when invalid constraints are attempted.
4921        // Example: coincident constraint between an arc segment and a straight line segment
4922        // is geometrically invalid and should fail, but state should remain intact.
4923        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
4924        let program = Program::empty();
4925
4926        let mut frontend = FrontendState::new();
4927        frontend.program = program;
4928
4929        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4930        let mock_ctx = ExecutorContext::new_mock(None).await;
4931        let version = Version(0);
4932
4933        let sketch_args = SketchCtor {
4934            on: PlaneName::Xy.to_string(),
4935        };
4936        let (_src_delta, _scene_delta, sketch_id) = frontend
4937            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4938            .await
4939            .unwrap();
4940
4941        // Add an arc segment
4942        let arc_ctor = ArcCtor {
4943            start: Point2d {
4944                x: Expr::Var(Number {
4945                    value: 0.0,
4946                    units: NumericSuffix::Mm,
4947                }),
4948                y: Expr::Var(Number {
4949                    value: 0.0,
4950                    units: NumericSuffix::Mm,
4951                }),
4952            },
4953            end: Point2d {
4954                x: Expr::Var(Number {
4955                    value: 10.0,
4956                    units: NumericSuffix::Mm,
4957                }),
4958                y: Expr::Var(Number {
4959                    value: 10.0,
4960                    units: NumericSuffix::Mm,
4961                }),
4962            },
4963            center: Point2d {
4964                x: Expr::Var(Number {
4965                    value: 10.0,
4966                    units: NumericSuffix::Mm,
4967                }),
4968                y: Expr::Var(Number {
4969                    value: 0.0,
4970                    units: NumericSuffix::Mm,
4971                }),
4972            },
4973            construction: None,
4974        };
4975        let (_src_delta, scene_delta) = frontend
4976            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
4977            .await
4978            .unwrap();
4979        // The arc is the last object in new_objects (after the 3 points: start, end, center)
4980        let arc_id = *scene_delta.new_objects.last().unwrap();
4981
4982        // Add a line segment
4983        let line_ctor = LineCtor {
4984            start: Point2d {
4985                x: Expr::Var(Number {
4986                    value: 20.0,
4987                    units: NumericSuffix::Mm,
4988                }),
4989                y: Expr::Var(Number {
4990                    value: 0.0,
4991                    units: NumericSuffix::Mm,
4992                }),
4993            },
4994            end: Point2d {
4995                x: Expr::Var(Number {
4996                    value: 30.0,
4997                    units: NumericSuffix::Mm,
4998                }),
4999                y: Expr::Var(Number {
5000                    value: 10.0,
5001                    units: NumericSuffix::Mm,
5002                }),
5003            },
5004            construction: None,
5005        };
5006        let (_src_delta, scene_delta) = frontend
5007            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
5008            .await
5009            .unwrap();
5010        // The line is the last object in new_objects (after the 2 points: start, end)
5011        let line_id = *scene_delta.new_objects.last().unwrap();
5012
5013        // Attempt to add an invalid coincident constraint between arc and line
5014        // This should fail during execution, but state should remain intact
5015        let constraint = Constraint::Coincident(Coincident {
5016            segments: vec![arc_id, line_id],
5017        });
5018        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
5019
5020        // The constraint addition should fail (invalid constraint)
5021        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
5022
5023        // Verify state is not corrupted by checking that we can still access the scene graph
5024        // and that the original segments are still present with their source ranges
5025        let sketch_object_after =
5026            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
5027        let sketch_after = expect_sketch(sketch_object_after);
5028
5029        // Verify both segments are still in the sketch
5030        assert!(
5031            sketch_after.segments.contains(&arc_id),
5032            "Arc segment should still exist after failed constraint"
5033        );
5034        assert!(
5035            sketch_after.segments.contains(&line_id),
5036            "Line segment should still exist after failed constraint"
5037        );
5038
5039        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
5040        let arc_obj = frontend
5041            .scene_graph
5042            .objects
5043            .get(arc_id.0)
5044            .expect("Arc object should still be accessible");
5045        let line_obj = frontend
5046            .scene_graph
5047            .objects
5048            .get(line_id.0)
5049            .expect("Line object should still be accessible");
5050
5051        // Verify source ranges are still valid (not corrupted)
5052        // Just verify that the objects are still accessible and have the expected types
5053        match &arc_obj.kind {
5054            ObjectKind::Segment {
5055                segment: Segment::Arc(_),
5056            } => {}
5057            _ => panic!("Arc object should still be an arc segment"),
5058        }
5059        match &line_obj.kind {
5060            ObjectKind::Segment {
5061                segment: Segment::Line(_),
5062            } => {}
5063            _ => panic!("Line object should still be a line segment"),
5064        }
5065
5066        ctx.close().await;
5067        mock_ctx.close().await;
5068    }
5069
5070    #[tokio::test(flavor = "multi_thread")]
5071    async fn test_distance_two_points() {
5072        let initial_source = "\
5073@settings(experimentalFeatures = allow)
5074
5075sketch(on = XY) {
5076  sketch2::point(at = [var 1, var 2])
5077  sketch2::point(at = [var 3, var 4])
5078}
5079";
5080
5081        let program = Program::parse(initial_source).unwrap().0.unwrap();
5082
5083        let mut frontend = FrontendState::new();
5084
5085        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5086        let mock_ctx = ExecutorContext::new_mock(None).await;
5087        let version = Version(0);
5088
5089        frontend.hack_set_program(&ctx, program).await.unwrap();
5090        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5091        let sketch_id = sketch_object.id;
5092        let sketch = expect_sketch(sketch_object);
5093        let point0_id = *sketch.segments.first().unwrap();
5094        let point1_id = *sketch.segments.get(1).unwrap();
5095
5096        let constraint = Constraint::Distance(Distance {
5097            points: vec![point0_id, point1_id],
5098            distance: Number {
5099                value: 2.0,
5100                units: NumericSuffix::Mm,
5101            },
5102            source: Default::default(),
5103        });
5104        let (src_delta, scene_delta) = frontend
5105            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5106            .await
5107            .unwrap();
5108        assert_eq!(
5109            src_delta.text.as_str(),
5110            // The lack indentation is a formatter bug.
5111            "\
5112@settings(experimentalFeatures = allow)
5113
5114sketch(on = XY) {
5115  point1 = sketch2::point(at = [var 1, var 2])
5116  point2 = sketch2::point(at = [var 3, var 4])
5117sketch2::distance([point1, point2]) == 2mm
5118}
5119"
5120        );
5121        assert_eq!(
5122            scene_delta.new_graph.objects.len(),
5123            5,
5124            "{:#?}",
5125            scene_delta.new_graph.objects
5126        );
5127
5128        ctx.close().await;
5129        mock_ctx.close().await;
5130    }
5131
5132    #[tokio::test(flavor = "multi_thread")]
5133    async fn test_horizontal_distance_two_points() {
5134        let initial_source = "\
5135@settings(experimentalFeatures = allow)
5136
5137sketch(on = XY) {
5138  sketch2::point(at = [var 1, var 2])
5139  sketch2::point(at = [var 3, var 4])
5140}
5141";
5142
5143        let program = Program::parse(initial_source).unwrap().0.unwrap();
5144
5145        let mut frontend = FrontendState::new();
5146
5147        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5148        let mock_ctx = ExecutorContext::new_mock(None).await;
5149        let version = Version(0);
5150
5151        frontend.hack_set_program(&ctx, program).await.unwrap();
5152        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5153        let sketch_id = sketch_object.id;
5154        let sketch = expect_sketch(sketch_object);
5155        let point0_id = *sketch.segments.first().unwrap();
5156        let point1_id = *sketch.segments.get(1).unwrap();
5157
5158        let constraint = Constraint::HorizontalDistance(Distance {
5159            points: vec![point0_id, point1_id],
5160            distance: Number {
5161                value: 2.0,
5162                units: NumericSuffix::Mm,
5163            },
5164            source: Default::default(),
5165        });
5166        let (src_delta, scene_delta) = frontend
5167            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5168            .await
5169            .unwrap();
5170        assert_eq!(
5171            src_delta.text.as_str(),
5172            // The lack indentation is a formatter bug.
5173            "\
5174@settings(experimentalFeatures = allow)
5175
5176sketch(on = XY) {
5177  point1 = sketch2::point(at = [var 1, var 2])
5178  point2 = sketch2::point(at = [var 3, var 4])
5179sketch2::horizontalDistance([point1, point2]) == 2mm
5180}
5181"
5182        );
5183        assert_eq!(
5184            scene_delta.new_graph.objects.len(),
5185            5,
5186            "{:#?}",
5187            scene_delta.new_graph.objects
5188        );
5189
5190        ctx.close().await;
5191        mock_ctx.close().await;
5192    }
5193
5194    #[tokio::test(flavor = "multi_thread")]
5195    async fn test_radius_single_arc_segment() {
5196        let initial_source = "\
5197@settings(experimentalFeatures = allow)
5198
5199sketch(on = XY) {
5200  sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5201}
5202";
5203
5204        let program = Program::parse(initial_source).unwrap().0.unwrap();
5205
5206        let mut frontend = FrontendState::new();
5207
5208        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5209        let mock_ctx = ExecutorContext::new_mock(None).await;
5210        let version = Version(0);
5211
5212        frontend.hack_set_program(&ctx, program).await.unwrap();
5213        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5214        let sketch_id = sketch_object.id;
5215        let sketch = expect_sketch(sketch_object);
5216        // Find the arc segment (not the points)
5217        let arc_id = sketch
5218            .segments
5219            .iter()
5220            .find(|&seg_id| {
5221                let obj = frontend.scene_graph.objects.get(seg_id.0);
5222                matches!(
5223                    obj.map(|o| &o.kind),
5224                    Some(ObjectKind::Segment {
5225                        segment: Segment::Arc(_)
5226                    })
5227                )
5228            })
5229            .unwrap();
5230
5231        let constraint = Constraint::Radius(Radius {
5232            arc: *arc_id,
5233            radius: Number {
5234                value: 5.0,
5235                units: NumericSuffix::Mm,
5236            },
5237        });
5238        let (src_delta, scene_delta) = frontend
5239            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5240            .await
5241            .unwrap();
5242        assert_eq!(
5243            src_delta.text.as_str(),
5244            // The lack indentation is a formatter bug.
5245            "\
5246@settings(experimentalFeatures = allow)
5247
5248sketch(on = XY) {
5249  arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5250sketch2::radius(arc1) == 5mm
5251}
5252"
5253        );
5254        assert_eq!(
5255            scene_delta.new_graph.objects.len(),
5256            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
5257            "{:#?}",
5258            scene_delta.new_graph.objects
5259        );
5260
5261        ctx.close().await;
5262        mock_ctx.close().await;
5263    }
5264
5265    #[tokio::test(flavor = "multi_thread")]
5266    async fn test_vertical_distance_two_points() {
5267        let initial_source = "\
5268@settings(experimentalFeatures = allow)
5269
5270sketch(on = XY) {
5271  sketch2::point(at = [var 1, var 2])
5272  sketch2::point(at = [var 3, var 4])
5273}
5274";
5275
5276        let program = Program::parse(initial_source).unwrap().0.unwrap();
5277
5278        let mut frontend = FrontendState::new();
5279
5280        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5281        let mock_ctx = ExecutorContext::new_mock(None).await;
5282        let version = Version(0);
5283
5284        frontend.hack_set_program(&ctx, program).await.unwrap();
5285        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5286        let sketch_id = sketch_object.id;
5287        let sketch = expect_sketch(sketch_object);
5288        let point0_id = *sketch.segments.first().unwrap();
5289        let point1_id = *sketch.segments.get(1).unwrap();
5290
5291        let constraint = Constraint::VerticalDistance(Distance {
5292            points: vec![point0_id, point1_id],
5293            distance: Number {
5294                value: 2.0,
5295                units: NumericSuffix::Mm,
5296            },
5297            source: Default::default(),
5298        });
5299        let (src_delta, scene_delta) = frontend
5300            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5301            .await
5302            .unwrap();
5303        assert_eq!(
5304            src_delta.text.as_str(),
5305            // The lack indentation is a formatter bug.
5306            "\
5307@settings(experimentalFeatures = allow)
5308
5309sketch(on = XY) {
5310  point1 = sketch2::point(at = [var 1, var 2])
5311  point2 = sketch2::point(at = [var 3, var 4])
5312sketch2::verticalDistance([point1, point2]) == 2mm
5313}
5314"
5315        );
5316        assert_eq!(
5317            scene_delta.new_graph.objects.len(),
5318            5,
5319            "{:#?}",
5320            scene_delta.new_graph.objects
5321        );
5322
5323        ctx.close().await;
5324        mock_ctx.close().await;
5325    }
5326
5327    #[tokio::test(flavor = "multi_thread")]
5328    async fn test_radius_error_cases() {
5329        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5330        let mock_ctx = ExecutorContext::new_mock(None).await;
5331        let version = Version(0);
5332
5333        // Test: Single point should error
5334        let initial_source_point = "\
5335@settings(experimentalFeatures = allow)
5336
5337sketch(on = XY) {
5338  sketch2::point(at = [var 1, var 2])
5339}
5340";
5341        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5342        let mut frontend_point = FrontendState::new();
5343        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5344        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5345        let sketch_id_point = sketch_object_point.id;
5346        let sketch_point = expect_sketch(sketch_object_point);
5347        let point_id = *sketch_point.segments.first().unwrap();
5348
5349        let constraint_point = Constraint::Radius(Radius {
5350            arc: point_id,
5351            radius: Number {
5352                value: 5.0,
5353                units: NumericSuffix::Mm,
5354            },
5355        });
5356        let result_point = frontend_point
5357            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5358            .await;
5359        assert!(result_point.is_err(), "Single point should error for radius");
5360
5361        // Test: Single line segment should error (only arc segments supported)
5362        let initial_source_line = "\
5363@settings(experimentalFeatures = allow)
5364
5365sketch(on = XY) {
5366  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5367}
5368";
5369        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5370        let mut frontend_line = FrontendState::new();
5371        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5372        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5373        let sketch_id_line = sketch_object_line.id;
5374        let sketch_line = expect_sketch(sketch_object_line);
5375        let line_id = *sketch_line.segments.first().unwrap();
5376
5377        let constraint_line = Constraint::Radius(Radius {
5378            arc: line_id,
5379            radius: Number {
5380                value: 5.0,
5381                units: NumericSuffix::Mm,
5382            },
5383        });
5384        let result_line = frontend_line
5385            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5386            .await;
5387        assert!(result_line.is_err(), "Single line segment should error for radius");
5388
5389        ctx.close().await;
5390        mock_ctx.close().await;
5391    }
5392
5393    #[tokio::test(flavor = "multi_thread")]
5394    async fn test_diameter_single_arc_segment() {
5395        let initial_source = "\
5396@settings(experimentalFeatures = allow)
5397
5398sketch(on = XY) {
5399  sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5400}
5401";
5402
5403        let program = Program::parse(initial_source).unwrap().0.unwrap();
5404
5405        let mut frontend = FrontendState::new();
5406
5407        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5408        let mock_ctx = ExecutorContext::new_mock(None).await;
5409        let version = Version(0);
5410
5411        frontend.hack_set_program(&ctx, program).await.unwrap();
5412        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5413        let sketch_id = sketch_object.id;
5414        let sketch = expect_sketch(sketch_object);
5415        // Find the arc segment (not the points)
5416        let arc_id = sketch
5417            .segments
5418            .iter()
5419            .find(|&seg_id| {
5420                let obj = frontend.scene_graph.objects.get(seg_id.0);
5421                matches!(
5422                    obj.map(|o| &o.kind),
5423                    Some(ObjectKind::Segment {
5424                        segment: Segment::Arc(_)
5425                    })
5426                )
5427            })
5428            .unwrap();
5429
5430        let constraint = Constraint::Diameter(Diameter {
5431            arc: *arc_id,
5432            diameter: Number {
5433                value: 10.0,
5434                units: NumericSuffix::Mm,
5435            },
5436        });
5437        let (src_delta, scene_delta) = frontend
5438            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5439            .await
5440            .unwrap();
5441        assert_eq!(
5442            src_delta.text.as_str(),
5443            // The lack indentation is a formatter bug.
5444            "\
5445@settings(experimentalFeatures = allow)
5446
5447sketch(on = XY) {
5448  arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5449sketch2::diameter(arc1) == 10mm
5450}
5451"
5452        );
5453        assert_eq!(
5454            scene_delta.new_graph.objects.len(),
5455            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
5456            "{:#?}",
5457            scene_delta.new_graph.objects
5458        );
5459
5460        ctx.close().await;
5461        mock_ctx.close().await;
5462    }
5463
5464    #[tokio::test(flavor = "multi_thread")]
5465    async fn test_diameter_error_cases() {
5466        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5467        let mock_ctx = ExecutorContext::new_mock(None).await;
5468        let version = Version(0);
5469
5470        // Test: Single point should error
5471        let initial_source_point = "\
5472@settings(experimentalFeatures = allow)
5473
5474sketch(on = XY) {
5475  sketch2::point(at = [var 1, var 2])
5476}
5477";
5478        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5479        let mut frontend_point = FrontendState::new();
5480        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5481        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5482        let sketch_id_point = sketch_object_point.id;
5483        let sketch_point = expect_sketch(sketch_object_point);
5484        let point_id = *sketch_point.segments.first().unwrap();
5485
5486        let constraint_point = Constraint::Diameter(Diameter {
5487            arc: point_id,
5488            diameter: Number {
5489                value: 10.0,
5490                units: NumericSuffix::Mm,
5491            },
5492        });
5493        let result_point = frontend_point
5494            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5495            .await;
5496        assert!(result_point.is_err(), "Single point should error for diameter");
5497
5498        // Test: Single line segment should error (only arc segments supported)
5499        let initial_source_line = "\
5500@settings(experimentalFeatures = allow)
5501
5502sketch(on = XY) {
5503  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5504}
5505";
5506        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5507        let mut frontend_line = FrontendState::new();
5508        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5509        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5510        let sketch_id_line = sketch_object_line.id;
5511        let sketch_line = expect_sketch(sketch_object_line);
5512        let line_id = *sketch_line.segments.first().unwrap();
5513
5514        let constraint_line = Constraint::Diameter(Diameter {
5515            arc: line_id,
5516            diameter: Number {
5517                value: 10.0,
5518                units: NumericSuffix::Mm,
5519            },
5520        });
5521        let result_line = frontend_line
5522            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5523            .await;
5524        assert!(result_line.is_err(), "Single line segment should error for diameter");
5525
5526        ctx.close().await;
5527        mock_ctx.close().await;
5528    }
5529
5530    #[tokio::test(flavor = "multi_thread")]
5531    async fn test_line_horizontal() {
5532        let initial_source = "\
5533@settings(experimentalFeatures = allow)
5534
5535sketch(on = XY) {
5536  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5537}
5538";
5539
5540        let program = Program::parse(initial_source).unwrap().0.unwrap();
5541
5542        let mut frontend = FrontendState::new();
5543
5544        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5545        let mock_ctx = ExecutorContext::new_mock(None).await;
5546        let version = Version(0);
5547
5548        frontend.hack_set_program(&ctx, program).await.unwrap();
5549        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5550        let sketch_id = sketch_object.id;
5551        let sketch = expect_sketch(sketch_object);
5552        let line1_id = *sketch.segments.get(2).unwrap();
5553
5554        let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
5555        let (src_delta, scene_delta) = frontend
5556            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5557            .await
5558            .unwrap();
5559        assert_eq!(
5560            src_delta.text.as_str(),
5561            "\
5562@settings(experimentalFeatures = allow)
5563
5564sketch(on = XY) {
5565  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5566  sketch2::horizontal(line1)
5567}
5568"
5569        );
5570        assert_eq!(
5571            scene_delta.new_graph.objects.len(),
5572            6,
5573            "{:#?}",
5574            scene_delta.new_graph.objects
5575        );
5576
5577        ctx.close().await;
5578        mock_ctx.close().await;
5579    }
5580
5581    #[tokio::test(flavor = "multi_thread")]
5582    async fn test_line_vertical() {
5583        let initial_source = "\
5584@settings(experimentalFeatures = allow)
5585
5586sketch(on = XY) {
5587  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5588}
5589";
5590
5591        let program = Program::parse(initial_source).unwrap().0.unwrap();
5592
5593        let mut frontend = FrontendState::new();
5594
5595        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5596        let mock_ctx = ExecutorContext::new_mock(None).await;
5597        let version = Version(0);
5598
5599        frontend.hack_set_program(&ctx, program).await.unwrap();
5600        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5601        let sketch_id = sketch_object.id;
5602        let sketch = expect_sketch(sketch_object);
5603        let line1_id = *sketch.segments.get(2).unwrap();
5604
5605        let constraint = Constraint::Vertical(Vertical { line: line1_id });
5606        let (src_delta, scene_delta) = frontend
5607            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5608            .await
5609            .unwrap();
5610        assert_eq!(
5611            src_delta.text.as_str(),
5612            "\
5613@settings(experimentalFeatures = allow)
5614
5615sketch(on = XY) {
5616  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5617  sketch2::vertical(line1)
5618}
5619"
5620        );
5621        assert_eq!(
5622            scene_delta.new_graph.objects.len(),
5623            6,
5624            "{:#?}",
5625            scene_delta.new_graph.objects
5626        );
5627
5628        ctx.close().await;
5629        mock_ctx.close().await;
5630    }
5631
5632    #[tokio::test(flavor = "multi_thread")]
5633    async fn test_lines_equal_length() {
5634        let initial_source = "\
5635@settings(experimentalFeatures = allow)
5636
5637sketch(on = XY) {
5638  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5639  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5640}
5641";
5642
5643        let program = Program::parse(initial_source).unwrap().0.unwrap();
5644
5645        let mut frontend = FrontendState::new();
5646
5647        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5648        let mock_ctx = ExecutorContext::new_mock(None).await;
5649        let version = Version(0);
5650
5651        frontend.hack_set_program(&ctx, program).await.unwrap();
5652        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5653        let sketch_id = sketch_object.id;
5654        let sketch = expect_sketch(sketch_object);
5655        let line1_id = *sketch.segments.get(2).unwrap();
5656        let line2_id = *sketch.segments.get(5).unwrap();
5657
5658        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
5659            lines: vec![line1_id, line2_id],
5660        });
5661        let (src_delta, scene_delta) = frontend
5662            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5663            .await
5664            .unwrap();
5665        assert_eq!(
5666            src_delta.text.as_str(),
5667            "\
5668@settings(experimentalFeatures = allow)
5669
5670sketch(on = XY) {
5671  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5672  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5673  sketch2::equalLength([line1, line2])
5674}
5675"
5676        );
5677        assert_eq!(
5678            scene_delta.new_graph.objects.len(),
5679            9,
5680            "{:#?}",
5681            scene_delta.new_graph.objects
5682        );
5683
5684        ctx.close().await;
5685        mock_ctx.close().await;
5686    }
5687
5688    #[tokio::test(flavor = "multi_thread")]
5689    async fn test_lines_parallel() {
5690        let initial_source = "\
5691@settings(experimentalFeatures = allow)
5692
5693sketch(on = XY) {
5694  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5695  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5696}
5697";
5698
5699        let program = Program::parse(initial_source).unwrap().0.unwrap();
5700
5701        let mut frontend = FrontendState::new();
5702
5703        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5704        let mock_ctx = ExecutorContext::new_mock(None).await;
5705        let version = Version(0);
5706
5707        frontend.hack_set_program(&ctx, program).await.unwrap();
5708        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5709        let sketch_id = sketch_object.id;
5710        let sketch = expect_sketch(sketch_object);
5711        let line1_id = *sketch.segments.get(2).unwrap();
5712        let line2_id = *sketch.segments.get(5).unwrap();
5713
5714        let constraint = Constraint::Parallel(Parallel {
5715            lines: vec![line1_id, line2_id],
5716        });
5717        let (src_delta, scene_delta) = frontend
5718            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5719            .await
5720            .unwrap();
5721        assert_eq!(
5722            src_delta.text.as_str(),
5723            "\
5724@settings(experimentalFeatures = allow)
5725
5726sketch(on = XY) {
5727  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5728  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5729  sketch2::parallel([line1, line2])
5730}
5731"
5732        );
5733        assert_eq!(
5734            scene_delta.new_graph.objects.len(),
5735            9,
5736            "{:#?}",
5737            scene_delta.new_graph.objects
5738        );
5739
5740        ctx.close().await;
5741        mock_ctx.close().await;
5742    }
5743
5744    #[tokio::test(flavor = "multi_thread")]
5745    async fn test_lines_perpendicular() {
5746        let initial_source = "\
5747@settings(experimentalFeatures = allow)
5748
5749sketch(on = XY) {
5750  sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5751  sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5752}
5753";
5754
5755        let program = Program::parse(initial_source).unwrap().0.unwrap();
5756
5757        let mut frontend = FrontendState::new();
5758
5759        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5760        let mock_ctx = ExecutorContext::new_mock(None).await;
5761        let version = Version(0);
5762
5763        frontend.hack_set_program(&ctx, program).await.unwrap();
5764        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5765        let sketch_id = sketch_object.id;
5766        let sketch = expect_sketch(sketch_object);
5767        let line1_id = *sketch.segments.get(2).unwrap();
5768        let line2_id = *sketch.segments.get(5).unwrap();
5769
5770        let constraint = Constraint::Perpendicular(Perpendicular {
5771            lines: vec![line1_id, line2_id],
5772        });
5773        let (src_delta, scene_delta) = frontend
5774            .add_constraint(&mock_ctx, version, sketch_id, constraint)
5775            .await
5776            .unwrap();
5777        assert_eq!(
5778            src_delta.text.as_str(),
5779            "\
5780@settings(experimentalFeatures = allow)
5781
5782sketch(on = XY) {
5783  line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5784  line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5785  sketch2::perpendicular([line1, line2])
5786}
5787"
5788        );
5789        assert_eq!(
5790            scene_delta.new_graph.objects.len(),
5791            9,
5792            "{:#?}",
5793            scene_delta.new_graph.objects
5794        );
5795
5796        ctx.close().await;
5797        mock_ctx.close().await;
5798    }
5799
5800    #[tokio::test(flavor = "multi_thread")]
5801    async fn test_sketch_on_face_simple() {
5802        let initial_source = "\
5803@settings(experimentalFeatures = allow)
5804
5805len = 2mm
5806cube = startSketchOn(XY)
5807  |> startProfile(at = [0, 0])
5808  |> line(end = [len, 0], tag = $side)
5809  |> line(end = [0, len])
5810  |> line(end = [-len, 0])
5811  |> line(end = [0, -len])
5812  |> close()
5813  |> extrude(length = len)
5814
5815face = faceOf(cube, face = side)
5816";
5817
5818        let program = Program::parse(initial_source).unwrap().0.unwrap();
5819
5820        let mut frontend = FrontendState::new();
5821
5822        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5823        let mock_ctx = ExecutorContext::new_mock(None).await;
5824        let version = Version(0);
5825
5826        frontend.hack_set_program(&ctx, program).await.unwrap();
5827        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
5828        let face_id = face_object.id;
5829
5830        let sketch_args = SketchCtor { on: "face".to_owned() };
5831        let (_src_delta, scene_delta, sketch_id) = frontend
5832            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5833            .await
5834            .unwrap();
5835        assert_eq!(sketch_id, ObjectId(2));
5836        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5837        let sketch_object = &scene_delta.new_graph.objects[2];
5838        assert_eq!(sketch_object.id, ObjectId(2));
5839        assert_eq!(
5840            sketch_object.kind,
5841            ObjectKind::Sketch(Sketch {
5842                args: SketchCtor { on: "face".to_owned() },
5843                plane: face_id,
5844                segments: vec![],
5845                constraints: vec![],
5846            })
5847        );
5848        assert_eq!(scene_delta.new_graph.objects.len(), 3);
5849
5850        ctx.close().await;
5851        mock_ctx.close().await;
5852    }
5853
5854    #[tokio::test(flavor = "multi_thread")]
5855    async fn test_sketch_on_plane_incremental() {
5856        let initial_source = "\
5857@settings(experimentalFeatures = allow)
5858
5859len = 2mm
5860cube = startSketchOn(XY)
5861  |> startProfile(at = [0, 0])
5862  |> line(end = [len, 0], tag = $side)
5863  |> line(end = [0, len])
5864  |> line(end = [-len, 0])
5865  |> line(end = [0, -len])
5866  |> close()
5867  |> extrude(length = len)
5868
5869plane = planeOf(cube, face = side)
5870";
5871
5872        let program = Program::parse(initial_source).unwrap().0.unwrap();
5873
5874        let mut frontend = FrontendState::new();
5875
5876        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5877        let mock_ctx = ExecutorContext::new_mock(None).await;
5878        let version = Version(0);
5879
5880        frontend.hack_set_program(&ctx, program).await.unwrap();
5881        // Find the last plane since the first plane is the XY plane.
5882        let plane_object = frontend
5883            .scene_graph
5884            .objects
5885            .iter()
5886            .rev()
5887            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
5888            .unwrap();
5889        let plane_id = plane_object.id;
5890
5891        let sketch_args = SketchCtor { on: "plane".to_owned() };
5892        let (src_delta, scene_delta, sketch_id) = frontend
5893            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5894            .await
5895            .unwrap();
5896        assert_eq!(
5897            src_delta.text.as_str(),
5898            "\
5899@settings(experimentalFeatures = allow)
5900
5901len = 2mm
5902cube = startSketchOn(XY)
5903  |> startProfile(at = [0, 0])
5904  |> line(end = [len, 0], tag = $side)
5905  |> line(end = [0, len])
5906  |> line(end = [-len, 0])
5907  |> line(end = [0, -len])
5908  |> close()
5909  |> extrude(length = len)
5910
5911plane = planeOf(cube, face = side)
5912sketch(on = plane) {
5913}
5914"
5915        );
5916        assert_eq!(sketch_id, ObjectId(2));
5917        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5918        let sketch_object = &scene_delta.new_graph.objects[2];
5919        assert_eq!(sketch_object.id, ObjectId(2));
5920        assert_eq!(
5921            sketch_object.kind,
5922            ObjectKind::Sketch(Sketch {
5923                args: SketchCtor { on: "plane".to_owned() },
5924                plane: plane_id,
5925                segments: vec![],
5926                constraints: vec![],
5927            })
5928        );
5929        assert_eq!(scene_delta.new_graph.objects.len(), 3);
5930
5931        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
5932        assert_eq!(plane_object.id, plane_id);
5933        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
5934
5935        ctx.close().await;
5936        mock_ctx.close().await;
5937    }
5938
5939    #[tokio::test(flavor = "multi_thread")]
5940    async fn test_multiple_sketch_blocks() {
5941        let initial_source = "\
5942@settings(experimentalFeatures = allow)
5943
5944// Cube that requires the engine.
5945width = 2
5946sketch001 = startSketchOn(XY)
5947profile001 = startProfile(sketch001, at = [0, 0])
5948  |> yLine(length = width, tag = $seg1)
5949  |> xLine(length = width)
5950  |> yLine(length = -width)
5951  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5952  |> close()
5953extrude001 = extrude(profile001, length = width)
5954
5955// Get a value that requires the engine.
5956x = segLen(seg1)
5957
5958// Triangle with side length 2*x.
5959sketch(on = XY) {
5960  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5961  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5962  sketch2::coincident([line1.end, line2.start])
5963  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5964  sketch2::coincident([line2.end, line3.start])
5965  sketch2::coincident([line3.end, line1.start])
5966  sketch2::equalLength([line3, line1])
5967  sketch2::equalLength([line1, line2])
5968sketch2::distance([line1.start, line1.end]) == 2*x
5969}
5970
5971// Line segment with length x.
5972sketch2 = sketch(on = XY) {
5973  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5974sketch2::distance([line1.start, line1.end]) == x
5975}
5976";
5977
5978        let program = Program::parse(initial_source).unwrap().0.unwrap();
5979
5980        let mut frontend = FrontendState::new();
5981
5982        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5983        let mock_ctx = ExecutorContext::new_mock(None).await;
5984        let version = Version(0);
5985        let project_id = ProjectId(0);
5986        let file_id = FileId(0);
5987
5988        frontend.hack_set_program(&ctx, program).await.unwrap();
5989        let sketch_objects = frontend
5990            .scene_graph
5991            .objects
5992            .iter()
5993            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
5994            .collect::<Vec<_>>();
5995        let sketch1_id = sketch_objects.first().unwrap().id;
5996        let sketch2_id = sketch_objects.get(1).unwrap().id;
5997        // First point in sketch1.
5998        let point1_id = ObjectId(sketch1_id.0 + 1);
5999        // First point in sketch2.
6000        let point2_id = ObjectId(sketch2_id.0 + 1);
6001
6002        // Edit the first sketch. Objects before the sketch block should be
6003        // present from execution cache so that we can sketch on prior planes,
6004        // for example. Objects after the first sketch block should not be
6005        // present since those statements are skipped in sketch mode.
6006        //
6007        // - startSketchOn(XY) Plane 1
6008        // - sketch on=XY Plane 1
6009        // - Sketch block 16
6010        let scene_delta = frontend
6011            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
6012            .await
6013            .unwrap();
6014        assert_eq!(
6015            scene_delta.new_graph.objects.len(),
6016            18,
6017            "{:#?}",
6018            scene_delta.new_graph.objects
6019        );
6020
6021        // Edit a point in the first sketch.
6022        let point_ctor = PointCtor {
6023            position: Point2d {
6024                x: Expr::Var(Number {
6025                    value: 1.0,
6026                    units: NumericSuffix::Mm,
6027                }),
6028                y: Expr::Var(Number {
6029                    value: 2.0,
6030                    units: NumericSuffix::Mm,
6031                }),
6032            },
6033        };
6034        let segments = vec![ExistingSegmentCtor {
6035            id: point1_id,
6036            ctor: SegmentCtor::Point(point_ctor),
6037        }];
6038        let (src_delta, _) = frontend
6039            .edit_segments(&mock_ctx, version, sketch1_id, segments)
6040            .await
6041            .unwrap();
6042        // Only the first sketch block changes.
6043        assert_eq!(
6044            src_delta.text.as_str(),
6045            "\
6046@settings(experimentalFeatures = allow)
6047
6048// Cube that requires the engine.
6049width = 2
6050sketch001 = startSketchOn(XY)
6051profile001 = startProfile(sketch001, at = [0, 0])
6052  |> yLine(length = width, tag = $seg1)
6053  |> xLine(length = width)
6054  |> yLine(length = -width)
6055  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6056  |> close()
6057extrude001 = extrude(profile001, length = width)
6058
6059// Get a value that requires the engine.
6060x = segLen(seg1)
6061
6062// Triangle with side length 2*x.
6063sketch(on = XY) {
6064  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
6065  line2 = sketch2::line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
6066  sketch2::coincident([line1.end, line2.start])
6067  line3 = sketch2::line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
6068  sketch2::coincident([line2.end, line3.start])
6069  sketch2::coincident([line3.end, line1.start])
6070  sketch2::equalLength([line3, line1])
6071  sketch2::equalLength([line1, line2])
6072sketch2::distance([line1.start, line1.end]) == 2 * x
6073}
6074
6075// Line segment with length x.
6076sketch2 = sketch(on = XY) {
6077  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
6078sketch2::distance([line1.start, line1.end]) == x
6079}
6080"
6081        );
6082
6083        // Execute mock to simulate drag end.
6084        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
6085        // Only the first sketch block changes.
6086        assert_eq!(
6087            src_delta.text.as_str(),
6088            "\
6089@settings(experimentalFeatures = allow)
6090
6091// Cube that requires the engine.
6092width = 2
6093sketch001 = startSketchOn(XY)
6094profile001 = startProfile(sketch001, at = [0, 0])
6095  |> yLine(length = width, tag = $seg1)
6096  |> xLine(length = width)
6097  |> yLine(length = -width)
6098  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6099  |> close()
6100extrude001 = extrude(profile001, length = width)
6101
6102// Get a value that requires the engine.
6103x = segLen(seg1)
6104
6105// Triangle with side length 2*x.
6106sketch(on = XY) {
6107  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6108  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6109  sketch2::coincident([line1.end, line2.start])
6110  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6111  sketch2::coincident([line2.end, line3.start])
6112  sketch2::coincident([line3.end, line1.start])
6113  sketch2::equalLength([line3, line1])
6114  sketch2::equalLength([line1, line2])
6115sketch2::distance([line1.start, line1.end]) == 2 * x
6116}
6117
6118// Line segment with length x.
6119sketch2 = sketch(on = XY) {
6120  line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
6121sketch2::distance([line1.start, line1.end]) == x
6122}
6123"
6124        );
6125        // Exit sketch. Objects from the entire program should be present.
6126        //
6127        // - startSketchOn(XY) Plane 1
6128        // - sketch on=XY Plane 1
6129        // - Sketch block 16
6130        // - sketch on=XY cached
6131        // - Sketch block 5
6132        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
6133        assert_eq!(scene.objects.len(), 23, "{:#?}", scene.objects);
6134
6135        // Edit the second sketch.
6136        //
6137        // - startSketchOn(XY) Plane 1
6138        // - sketch on=XY Plane 1
6139        // - Sketch block 16
6140        // - sketch on=XY cached
6141        // - Sketch block 5
6142        let scene_delta = frontend
6143            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
6144            .await
6145            .unwrap();
6146        assert_eq!(
6147            scene_delta.new_graph.objects.len(),
6148            23,
6149            "{:#?}",
6150            scene_delta.new_graph.objects
6151        );
6152
6153        // Edit a point in the second sketch.
6154        let point_ctor = PointCtor {
6155            position: Point2d {
6156                x: Expr::Var(Number {
6157                    value: 3.0,
6158                    units: NumericSuffix::Mm,
6159                }),
6160                y: Expr::Var(Number {
6161                    value: 4.0,
6162                    units: NumericSuffix::Mm,
6163                }),
6164            },
6165        };
6166        let segments = vec![ExistingSegmentCtor {
6167            id: point2_id,
6168            ctor: SegmentCtor::Point(point_ctor),
6169        }];
6170        let (src_delta, _) = frontend
6171            .edit_segments(&mock_ctx, version, sketch2_id, segments)
6172            .await
6173            .unwrap();
6174        // Only the second sketch block changes.
6175        assert_eq!(
6176            src_delta.text.as_str(),
6177            "\
6178@settings(experimentalFeatures = allow)
6179
6180// Cube that requires the engine.
6181width = 2
6182sketch001 = startSketchOn(XY)
6183profile001 = startProfile(sketch001, at = [0, 0])
6184  |> yLine(length = width, tag = $seg1)
6185  |> xLine(length = width)
6186  |> yLine(length = -width)
6187  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6188  |> close()
6189extrude001 = extrude(profile001, length = width)
6190
6191// Get a value that requires the engine.
6192x = segLen(seg1)
6193
6194// Triangle with side length 2*x.
6195sketch(on = XY) {
6196  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6197  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6198  sketch2::coincident([line1.end, line2.start])
6199  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6200  sketch2::coincident([line2.end, line3.start])
6201  sketch2::coincident([line3.end, line1.start])
6202  sketch2::equalLength([line3, line1])
6203  sketch2::equalLength([line1, line2])
6204sketch2::distance([line1.start, line1.end]) == 2 * x
6205}
6206
6207// Line segment with length x.
6208sketch2 = sketch(on = XY) {
6209  line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
6210sketch2::distance([line1.start, line1.end]) == x
6211}
6212"
6213        );
6214
6215        // Execute mock to simulate drag end.
6216        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
6217        // Only the second sketch block changes.
6218        assert_eq!(
6219            src_delta.text.as_str(),
6220            "\
6221@settings(experimentalFeatures = allow)
6222
6223// Cube that requires the engine.
6224width = 2
6225sketch001 = startSketchOn(XY)
6226profile001 = startProfile(sketch001, at = [0, 0])
6227  |> yLine(length = width, tag = $seg1)
6228  |> xLine(length = width)
6229  |> yLine(length = -width)
6230  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6231  |> close()
6232extrude001 = extrude(profile001, length = width)
6233
6234// Get a value that requires the engine.
6235x = segLen(seg1)
6236
6237// Triangle with side length 2*x.
6238sketch(on = XY) {
6239  line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6240  line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6241  sketch2::coincident([line1.end, line2.start])
6242  line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6243  sketch2::coincident([line2.end, line3.start])
6244  sketch2::coincident([line3.end, line1.start])
6245  sketch2::equalLength([line3, line1])
6246  sketch2::equalLength([line1, line2])
6247sketch2::distance([line1.start, line1.end]) == 2 * x
6248}
6249
6250// Line segment with length x.
6251sketch2 = sketch(on = XY) {
6252  line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
6253sketch2::distance([line1.start, line1.end]) == x
6254}
6255"
6256        );
6257
6258        ctx.close().await;
6259        mock_ctx.close().await;
6260    }
6261}