Skip to main content

kcl_lib/
frontend.rs

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