Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2#[cfg(feature = "artifact-graph")]
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::collections::VecDeque;
7use std::ops::ControlFlow;
8
9use indexmap::IndexMap;
10use kcl_error::CompilationIssue;
11use kcl_error::SourceRange;
12use kittycad_modeling_cmds::units::UnitLength;
13use serde::Serialize;
14
15use crate::ExecOutcome;
16use crate::ExecutorContext;
17use crate::KclError;
18use crate::KclErrorWithOutputs;
19use crate::Program;
20use crate::collections::AhashIndexSet;
21use crate::execution::Artifact;
22use crate::execution::ArtifactGraph;
23use crate::execution::CapSubType;
24use crate::execution::MockConfig;
25use crate::execution::SKETCH_BLOCK_PARAM_ON;
26use crate::execution::cache::SketchModeState;
27use crate::execution::cache::clear_mem_cache;
28use crate::execution::cache::read_old_memory;
29use crate::execution::cache::write_old_memory;
30use crate::fmt::format_number_literal;
31use crate::front::Angle;
32use crate::front::ArcCtor;
33use crate::front::CircleCtor;
34use crate::front::Distance;
35use crate::front::EqualRadius;
36use crate::front::Error;
37use crate::front::ExecResult;
38use crate::front::FixedPoint;
39use crate::front::Freedom;
40use crate::front::LinesEqualLength;
41use crate::front::Midpoint;
42use crate::front::Object;
43use crate::front::Parallel;
44use crate::front::Perpendicular;
45use crate::front::PointCtor;
46use crate::front::Symmetric;
47use crate::front::Tangent;
48use crate::frontend::api::Expr;
49use crate::frontend::api::FileId;
50use crate::frontend::api::Number;
51use crate::frontend::api::ObjectId;
52use crate::frontend::api::ObjectKind;
53use crate::frontend::api::Plane;
54use crate::frontend::api::ProjectId;
55use crate::frontend::api::RestoreSketchCheckpointOutcome;
56use crate::frontend::api::SceneGraph;
57use crate::frontend::api::SceneGraphDelta;
58use crate::frontend::api::SketchCheckpointId;
59use crate::frontend::api::SourceDelta;
60use crate::frontend::api::SourceRef;
61use crate::frontend::api::Version;
62use crate::frontend::modify::find_defined_names;
63use crate::frontend::modify::next_free_name;
64use crate::frontend::modify::next_free_name_with_padding;
65use crate::frontend::sketch::Coincident;
66use crate::frontend::sketch::Constraint;
67use crate::frontend::sketch::ConstraintSegment;
68use crate::frontend::sketch::Diameter;
69use crate::frontend::sketch::ExistingSegmentCtor;
70use crate::frontend::sketch::Horizontal;
71use crate::frontend::sketch::LineCtor;
72use crate::frontend::sketch::Point2d;
73use crate::frontend::sketch::Radius;
74use crate::frontend::sketch::Segment;
75use crate::frontend::sketch::SegmentCtor;
76use crate::frontend::sketch::SketchApi;
77use crate::frontend::sketch::SketchCtor;
78use crate::frontend::sketch::Vertical;
79use crate::frontend::traverse::MutateBodyItem;
80use crate::frontend::traverse::TraversalReturn;
81use crate::frontend::traverse::Visitor;
82use crate::frontend::traverse::dfs_mut;
83use crate::id::IncIdGenerator;
84use crate::parsing::ast::types as ast;
85use crate::pretty::NumericSuffix;
86use crate::std::constraints::LinesAtAngleKind;
87#[cfg(feature = "artifact-graph")]
88use crate::walk::Node;
89use crate::walk::NodeMut;
90use crate::walk::Visitable;
91
92pub(crate) mod api;
93pub(crate) mod modify;
94pub(crate) mod sketch;
95
96pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
97
98#[derive(Debug, Clone)]
99struct SketchCheckpoint {
100    id: SketchCheckpointId,
101    source: SourceDelta,
102    program: Program,
103    scene_graph: SceneGraph,
104    exec_outcome: ExecOutcome,
105    point_freedom_cache: HashMap<ObjectId, Freedom>,
106    mock_memory: Option<SketchModeState>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum SketchVarUpdateMode {
111    /// Keep solved values as transient warm-start state only. Used for drag
112    /// previews and other intermediate edits.
113    WarmStartOnly,
114    /// Serialize the settled solved values back into KCL. Used at interaction
115    /// boundaries where source should represent the displayed sketch.
116    CommitSolvedVars,
117    /// Serialize solved values back into KCL after solving from transient
118    /// warm-start state. Used at drag boundaries after preview edits.
119    CommitSolvedVarsFromWarmStart,
120}
121mod traverse;
122pub(crate) mod trim;
123
124struct ArcSizeConstraintParams {
125    points: Vec<ObjectId>,
126    function_name: &'static str,
127    value: f64,
128    units: NumericSuffix,
129    label_position: Option<Point2d<Number>>,
130    constraint_type_name: &'static str,
131}
132
133const POINT_FN: &str = "point";
134const POINT_AT_PARAM: &str = "at";
135const LINE_FN: &str = "line";
136const LINE_VARIABLE: &str = "line";
137const LINE_START_PARAM: &str = "start";
138const LINE_END_PARAM: &str = "end";
139const ARC_FN: &str = "arc";
140const ARC_VARIABLE: &str = "arc";
141const ARC_START_PARAM: &str = "start";
142const ARC_END_PARAM: &str = "end";
143const ARC_CENTER_PARAM: &str = "center";
144const CIRCLE_FN: &str = "circle";
145const CIRCLE_VARIABLE: &str = "circle";
146const CIRCLE_START_PARAM: &str = "start";
147const CIRCLE_CENTER_PARAM: &str = "center";
148const LABEL_POSITION_PARAM: &str = "labelPosition";
149
150const COINCIDENT_FN: &str = "coincident";
151const DIAMETER_FN: &str = "diameter";
152const DISTANCE_FN: &str = "distance";
153const FIXED_FN: &str = "fixed";
154const ANGLE_FN: &str = "angle";
155const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
156const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
157const EQUAL_LENGTH_FN: &str = "equalLength";
158const EQUAL_RADIUS_FN: &str = "equalRadius";
159const HORIZONTAL_FN: &str = "horizontal";
160const MIDPOINT_FN: &str = "midpoint";
161const MIDPOINT_POINT_PARAM: &str = "point";
162const RADIUS_FN: &str = "radius";
163const SYMMETRIC_FN: &str = "symmetric";
164const SYMMETRIC_AXIS_PARAM: &str = "axis";
165const TANGENT_FN: &str = "tangent";
166const VERTICAL_FN: &str = "vertical";
167
168const LINE_PROPERTY_START: &str = "start";
169const LINE_PROPERTY_END: &str = "end";
170
171const ARC_PROPERTY_START: &str = "start";
172const ARC_PROPERTY_END: &str = "end";
173const ARC_PROPERTY_CENTER: &str = "center";
174const CIRCLE_PROPERTY_START: &str = "start";
175const CIRCLE_PROPERTY_CENTER: &str = "center";
176
177const CONSTRUCTION_PARAM: &str = "construction";
178
179#[derive(Debug, Clone, Copy)]
180enum EditDeleteKind {
181    Edit,
182    DeleteNonSketch,
183}
184
185impl EditDeleteKind {
186    /// Returns true if this edit is any type of deletion.
187    fn is_delete(&self) -> bool {
188        match self {
189            EditDeleteKind::Edit => false,
190            EditDeleteKind::DeleteNonSketch => true,
191        }
192    }
193
194    fn to_change_kind(self) -> ChangeKind {
195        match self {
196            EditDeleteKind::Edit => ChangeKind::Edit,
197            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
198        }
199    }
200}
201
202struct ExecuteAfterEditOptions {
203    segment_ids_edited: AhashIndexSet<ObjectId>,
204    edit_kind: EditDeleteKind,
205    sketch_var_update_mode: SketchVarUpdateMode,
206}
207
208#[derive(Debug, Clone, Copy)]
209enum ChangeKind {
210    Add,
211    Edit,
212    Delete,
213    None,
214}
215
216#[derive(Debug, Clone, Serialize, ts_rs::TS)]
217#[ts(export, export_to = "FrontendApi.ts")]
218#[serde(tag = "type")]
219pub enum SetProgramOutcome {
220    #[serde(rename_all = "camelCase")]
221    Success {
222        scene_graph: Box<SceneGraph>,
223        exec_outcome: Box<ExecOutcome>,
224        checkpoint_id: Option<SketchCheckpointId>,
225    },
226    #[serde(rename_all = "camelCase")]
227    ExecFailure { error: Box<KclErrorWithOutputs> },
228}
229
230#[derive(Debug, Clone)]
231pub struct FrontendState {
232    program: Program,
233    scene_graph: SceneGraph,
234    /// Stores the last known freedom value for each point object.
235    /// This allows us to preserve freedom values when freedom analysis isn't run.
236    point_freedom_cache: HashMap<ObjectId, Freedom>,
237    /// Transient solver continuity state. During previews we warm-start from
238    /// the previous solution without treating those solved guesses as committed
239    /// source text.
240    sketch_var_warm_start_overrides: HashMap<ObjectId, Vec<f64>>,
241    next_sketch_var_update_mode: Option<SketchVarUpdateMode>,
242    sketch_checkpoints: VecDeque<SketchCheckpoint>,
243    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
244}
245
246impl Default for FrontendState {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252impl FrontendState {
253    pub fn new() -> Self {
254        Self {
255            program: Program::empty(),
256            scene_graph: SceneGraph {
257                project: ProjectId(0),
258                file: FileId(0),
259                version: Version(0),
260                objects: Default::default(),
261                settings: Default::default(),
262                sketch_mode: Default::default(),
263            },
264            point_freedom_cache: HashMap::new(),
265            sketch_var_warm_start_overrides: HashMap::new(),
266            next_sketch_var_update_mode: None,
267            sketch_checkpoints: VecDeque::new(),
268            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
269        }
270    }
271
272    /// Get a reference to the scene graph
273    pub fn scene_graph(&self) -> &SceneGraph {
274        &self.scene_graph
275    }
276
277    pub fn default_length_unit(&self) -> UnitLength {
278        self.program
279            .meta_settings()
280            .ok()
281            .flatten()
282            .map(|settings| settings.default_length_units)
283            .unwrap_or(UnitLength::Millimeters)
284    }
285
286    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
287        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
288
289        let checkpoint = SketchCheckpoint {
290            id: checkpoint_id,
291            source: SourceDelta {
292                text: source_from_ast(&self.program.ast),
293            },
294            program: self.program.clone(),
295            scene_graph: self.scene_graph.clone(),
296            exec_outcome,
297            point_freedom_cache: self.point_freedom_cache.clone(),
298            mock_memory: read_old_memory().await,
299        };
300
301        self.sketch_checkpoints.push_back(checkpoint);
302        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
303            self.sketch_checkpoints.pop_front();
304        }
305
306        Ok(checkpoint_id)
307    }
308
309    pub async fn restore_sketch_checkpoint(
310        &mut self,
311        checkpoint_id: SketchCheckpointId,
312    ) -> api::Result<RestoreSketchCheckpointOutcome> {
313        let checkpoint = self
314            .sketch_checkpoints
315            .iter()
316            .find(|checkpoint| checkpoint.id == checkpoint_id)
317            .cloned()
318            .ok_or_else(|| Error {
319                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
320            })?;
321
322        self.program = checkpoint.program;
323        self.scene_graph = checkpoint.scene_graph.clone();
324        self.point_freedom_cache = checkpoint.point_freedom_cache;
325        self.clear_sketch_var_warm_starts();
326
327        if let Some(mock_memory) = checkpoint.mock_memory {
328            write_old_memory(mock_memory).await;
329        } else {
330            clear_mem_cache().await;
331        }
332
333        Ok(RestoreSketchCheckpointOutcome {
334            source_delta: checkpoint.source,
335            scene_graph_delta: SceneGraphDelta {
336                new_graph: checkpoint.scene_graph,
337                new_objects: Vec::new(),
338                invalidates_ids: true,
339                exec_outcome: checkpoint.exec_outcome,
340            },
341        })
342    }
343
344    pub fn clear_sketch_checkpoints(&mut self) {
345        self.sketch_checkpoints.clear();
346    }
347
348    pub(crate) fn clear_sketch_var_warm_starts(&mut self) {
349        self.sketch_var_warm_start_overrides.clear();
350    }
351
352    pub async fn edit_segments_for_preview(
353        &mut self,
354        ctx: &ExecutorContext,
355        version: Version,
356        sketch: ObjectId,
357        segments: Vec<ExistingSegmentCtor>,
358    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
359        self.next_sketch_var_update_mode = Some(SketchVarUpdateMode::WarmStartOnly);
360        SketchApi::edit_segments(self, ctx, version, sketch, segments).await
361    }
362
363    pub async fn edit_segments_commit_from_preview(
364        &mut self,
365        ctx: &ExecutorContext,
366        version: Version,
367        sketch: ObjectId,
368        segments: Vec<ExistingSegmentCtor>,
369    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
370        self.next_sketch_var_update_mode = Some(SketchVarUpdateMode::CommitSolvedVarsFromWarmStart);
371        SketchApi::edit_segments(self, ctx, version, sketch, segments).await
372    }
373
374    pub async fn execute_mock_from_preview(
375        &mut self,
376        ctx: &ExecutorContext,
377        version: Version,
378        sketch: ObjectId,
379    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
380        let _ = version;
381        self.execute_mock_with_warm_starts(ctx, sketch, true).await
382    }
383
384    fn sketch_mock_config(
385        &self,
386        sketch: ObjectId,
387        freedom_analysis: bool,
388        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] segment_ids_edited: AhashIndexSet<
389            ObjectId,
390        >,
391        use_warm_starts: bool,
392    ) -> MockConfig {
393        let config = MockConfig {
394            sketch_block_id: Some(sketch),
395            freedom_analysis,
396            #[cfg(feature = "artifact-graph")]
397            segment_ids_edited,
398            ..Default::default()
399        };
400
401        match (use_warm_starts, self.sketch_var_warm_start_overrides.get(&sketch)) {
402            (true, Some(overrides)) => config.with_sketch_var_initial_guess_overrides(overrides.clone()),
403            _ => config,
404        }
405    }
406
407    async fn execute_mock_with_warm_starts(
408        &mut self,
409        ctx: &ExecutorContext,
410        sketch: ObjectId,
411        use_warm_starts: bool,
412    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
413        let sketch_block_ref =
414            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
415
416        let mut truncated_program = self.program.clone();
417        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
418            .map_err(KclErrorWithOutputs::no_outputs)?;
419
420        let mock_config = self.sketch_mock_config(sketch, true, Default::default(), use_warm_starts);
421        let run_outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
422        let outcome = self.update_state_after_exec(run_outcome, true);
423        self.replace_sketch_var_warm_starts(sketch, &outcome);
424        let new_source = self.commit_var_solutions_to_program(&outcome)?;
425
426        let src_delta = SourceDelta { text: new_source };
427        let scene_graph_delta = SceneGraphDelta {
428            new_graph: self.scene_graph.clone(),
429            new_objects: Default::default(),
430            invalidates_ids: false,
431            exec_outcome: outcome,
432        };
433        Ok((src_delta, scene_graph_delta))
434    }
435
436    fn replace_sketch_var_warm_starts(&mut self, sketch: ObjectId, outcome: &ExecOutcome) {
437        #[cfg(feature = "artifact-graph")]
438        {
439            let solution_by_range = outcome
440                .var_solutions
441                .iter()
442                .map(|(range, value)| (*range, value.value))
443                .collect::<HashMap<_, _>>();
444            let values = self
445                .sketch_var_initial_guesses_from_program(sketch, &solution_by_range)
446                .unwrap_or_else(|| outcome.var_solutions.iter().map(|(_, value)| value.value).collect());
447            self.sketch_var_warm_start_overrides.insert(sketch, values);
448        }
449        #[cfg(not(feature = "artifact-graph"))]
450        {
451            let _ = (sketch, outcome);
452        }
453    }
454
455    fn merge_committed_edit_sketch_var_warm_starts(
456        &mut self,
457        sketch: ObjectId,
458        outcome: &ExecOutcome,
459        segment_ids_edited: &AhashIndexSet<ObjectId>,
460    ) {
461        #[cfg(feature = "artifact-graph")]
462        {
463            let solution_by_range = outcome
464                .var_solutions
465                .iter()
466                .map(|(range, value)| (*range, value.value))
467                .collect::<HashMap<_, _>>();
468            let previous = self.sketch_var_warm_start_overrides.get(&sketch).cloned();
469            let Some(source_values) = self.sketch_var_source_values_from_program(sketch) else {
470                self.replace_sketch_var_warm_starts(sketch, outcome);
471                return;
472            };
473            let edited_var_indices = self.edited_sketch_var_indices(&source_values, segment_ids_edited);
474            let values = source_values
475                .into_iter()
476                .enumerate()
477                .map(|(index, (range, source_value))| {
478                    if edited_var_indices.contains(&index) || !solution_by_range.contains_key(&range) {
479                        source_value
480                    } else {
481                        previous
482                            .as_ref()
483                            .and_then(|values| values.get(index))
484                            .copied()
485                            .unwrap_or(source_value)
486                    }
487                })
488                .collect();
489            self.sketch_var_warm_start_overrides.insert(sketch, values);
490        }
491        #[cfg(not(feature = "artifact-graph"))]
492        {
493            let _ = (sketch, outcome, segment_ids_edited);
494        }
495    }
496
497    #[cfg(feature = "artifact-graph")]
498    fn edited_sketch_var_indices(
499        &self,
500        source_values: &[(SourceRange, f64)],
501        segment_ids_edited: &AhashIndexSet<ObjectId>,
502    ) -> HashSet<usize> {
503        let edited_ranges = segment_ids_edited
504            .iter()
505            .filter_map(|segment_id| self.scene_graph.objects.get(segment_id.0))
506            .filter_map(|object| source_ref_primary_range(&object.source))
507            .collect::<Vec<_>>();
508
509        source_values
510            .iter()
511            .enumerate()
512            .filter_map(|(index, (range, _))| {
513                edited_ranges
514                    .iter()
515                    .any(|edited_range| edited_range.contains_range(range))
516                    .then_some(index)
517            })
518            .collect()
519    }
520
521    #[cfg(feature = "artifact-graph")]
522    fn sketch_var_initial_guesses_from_program(
523        &self,
524        sketch: ObjectId,
525        solution_by_range: &HashMap<SourceRange, f64>,
526    ) -> Option<Vec<f64>> {
527        self.sketch_var_source_values_from_program(sketch).map(|source_values| {
528            source_values
529                .into_iter()
530                .map(|(range, source_value)| solution_by_range.get(&range).copied().unwrap_or(source_value))
531                .collect()
532        })
533    }
534
535    #[cfg(feature = "artifact-graph")]
536    fn sketch_var_source_values_from_program(&self, sketch: ObjectId) -> Option<Vec<(SourceRange, f64)>> {
537        let sketch_range = source_ref_primary_range(&self.scene_graph.objects.get(sketch.0)?.source)?;
538        let values = RefCell::new(Vec::new());
539        crate::walk::walk(&self.program.ast, |node| -> anyhow::Result<bool> {
540            let Node::SketchVar(sketch_var) = node else {
541                return Ok(true);
542            };
543            let Some(initial) = sketch_var.initial.as_ref() else {
544                return Ok(true);
545            };
546            let range = initial.as_source_range();
547            if sketch_range.contains_range(&range) {
548                values
549                    .borrow_mut()
550                    .push((range, sketch_var_initial_value_in_solver_units(initial)));
551            }
552            Ok(true)
553        })
554        .ok()?;
555        let values = values.into_inner();
556        (!values.is_empty()).then_some(values)
557    }
558
559    fn update_single_sketch_var_warm_starts(&mut self, outcome: &ExecOutcome) {
560        let mut sketch_ids = self.scene_graph.objects.iter().filter_map(|object| match object.kind {
561            ObjectKind::Sketch(_) => Some(object.id),
562            _ => None,
563        });
564        let Some(sketch_id) = sketch_ids.next() else {
565            return;
566        };
567        if sketch_ids.next().is_none() {
568            self.replace_sketch_var_warm_starts(sketch_id, outcome);
569        }
570    }
571
572    #[cfg(feature = "artifact-graph")]
573    fn source_with_committed_var_solutions(
574        &self,
575        outcome: &ExecOutcome,
576    ) -> ExecResult<(ast::Node<ast::Program>, String)> {
577        let mut new_ast = self.program.ast.clone();
578        for (var_range, value) in &outcome.var_solutions {
579            let rounded = value.round(3);
580            let source_ref = SourceRef::Simple {
581                range: *var_range,
582                node_path: None,
583            };
584            mutate_ast_node_by_source_ref(
585                &mut new_ast,
586                &source_ref,
587                AstMutateCommand::EditVarInitialValue { value: rounded },
588            )
589            .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
590        }
591        let source = source_from_ast(&new_ast);
592        Ok((new_ast, source))
593    }
594
595    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome) -> ExecResult<String> {
596        #[cfg(feature = "artifact-graph")]
597        {
598            let (new_ast, source) = self.source_with_committed_var_solutions(outcome)?;
599            self.program = Program {
600                ast: new_ast,
601                original_file_contents: source.clone(),
602            };
603            Ok(source)
604        }
605        #[cfg(not(feature = "artifact-graph"))]
606        {
607            let _ = outcome;
608            Ok(source_from_ast(&self.program.ast))
609        }
610    }
611}
612
613impl SketchApi for FrontendState {
614    async fn execute_mock(
615        &mut self,
616        ctx: &ExecutorContext,
617        _version: Version,
618        sketch: ObjectId,
619    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
620        self.execute_mock_with_warm_starts(ctx, sketch, false).await
621    }
622
623    async fn new_sketch(
624        &mut self,
625        ctx: &ExecutorContext,
626        _project: ProjectId,
627        _file: FileId,
628        _version: Version,
629        args: SketchCtor,
630    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
631        // TODO: Check version.
632
633        let mut new_ast = self.program.ast.clone();
634        // Create updated KCL source from args.
635        let mut plane_ast =
636            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
637        let mut defined_names = find_defined_names(&new_ast);
638        let is_face_of_expr = matches!(
639            &plane_ast,
640            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
641        );
642        if is_face_of_expr {
643            let face_name = next_free_name_with_padding("face", &defined_names)
644                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
645            let face_decl = ast::VariableDeclaration::new(
646                ast::VariableDeclarator::new(&face_name, plane_ast),
647                ast::ItemVisibility::Default,
648                ast::VariableKind::Const,
649            );
650            new_ast
651                .body
652                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
653                    face_decl,
654                ))));
655            defined_names.insert(face_name.clone());
656            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
657        }
658        let sketch_ast = ast::SketchBlock {
659            arguments: vec![ast::LabeledArg {
660                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
661                arg: plane_ast,
662            }],
663            body: Default::default(),
664            is_being_edited: false,
665            non_code_meta: Default::default(),
666            digest: None,
667        };
668        // Add a sketch block as a variable declaration directly, avoiding
669        // source-range mutation on a no-src node.
670        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
671            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
672        let sketch_decl = ast::VariableDeclaration::new(
673            ast::VariableDeclarator::new(
674                &sketch_name,
675                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
676            ),
677            ast::ItemVisibility::Default,
678            ast::VariableKind::Const,
679        );
680        new_ast
681            .body
682            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
683                sketch_decl,
684            ))));
685        // Convert to string source to create real source ranges.
686        let new_source = source_from_ast(&new_ast);
687        // Parse the new source.
688        let (new_program, errors) = Program::parse(&new_source)
689            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
690        if !errors.is_empty() {
691            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
692                "Error parsing KCL source after adding sketch: {errors:?}"
693            ))));
694        }
695        let Some(new_program) = new_program else {
696            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
697                "No AST produced after adding sketch".to_owned(),
698            )));
699        };
700
701        // Make sure to only set this if there are no errors.
702        self.program = new_program.clone();
703        self.clear_sketch_var_warm_starts();
704
705        // We need to do an engine execute so that the plane object gets created
706        // and is cached.
707        let outcome = ctx.run_with_caching(new_program.clone()).await?;
708        let freedom_analysis_ran = true;
709
710        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
711
712        let Some(sketch_id) = self
713            .scene_graph
714            .objects
715            .iter()
716            .filter_map(|object| match object.kind {
717                ObjectKind::Sketch(_) => Some(object.id),
718                _ => None,
719            })
720            .max_by_key(|id| id.0)
721        else {
722            return Err(KclErrorWithOutputs::from_error_outcome(
723                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
724                outcome,
725            ));
726        };
727        // Store the object in the scene.
728        self.scene_graph.sketch_mode = Some(sketch_id);
729
730        let src_delta = SourceDelta { text: new_source };
731        let scene_graph_delta = SceneGraphDelta {
732            new_graph: self.scene_graph.clone(),
733            invalidates_ids: false,
734            new_objects: vec![sketch_id],
735            exec_outcome: outcome,
736        };
737        Ok((src_delta, scene_graph_delta, sketch_id))
738    }
739
740    async fn edit_sketch(
741        &mut self,
742        ctx: &ExecutorContext,
743        _project: ProjectId,
744        _file: FileId,
745        _version: Version,
746        sketch: ObjectId,
747    ) -> ExecResult<SceneGraphDelta> {
748        // TODO: Check version.
749
750        // Look up existing sketch.
751        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
752            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
753        })?;
754        let ObjectKind::Sketch(_) = &sketch_object.kind else {
755            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
756                "Object is not a sketch, it is {}",
757                sketch_object.kind.human_friendly_kind_with_article()
758            ))));
759        };
760        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
761
762        // Enter sketch mode by setting the sketch_mode.
763        self.scene_graph.sketch_mode = Some(sketch);
764
765        // Truncate after the sketch block for mock execution.
766        let mut truncated_program = self.program.clone();
767        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
768            .map_err(KclErrorWithOutputs::no_outputs)?;
769
770        // Execute in mock mode to ensure state is up to date. The caller will
771        // want freedom analysis to display segments correctly.
772        let outcome = ctx
773            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
774            .await?;
775
776        // MockConfig::default() has freedom_analysis: true
777        let outcome = self.update_state_after_exec(outcome, true);
778        self.replace_sketch_var_warm_starts(sketch, &outcome);
779        let scene_graph_delta = SceneGraphDelta {
780            new_graph: self.scene_graph.clone(),
781            invalidates_ids: false,
782            new_objects: Vec::new(),
783            exec_outcome: outcome,
784        };
785        Ok(scene_graph_delta)
786    }
787
788    async fn exit_sketch(
789        &mut self,
790        ctx: &ExecutorContext,
791        _version: Version,
792        sketch: ObjectId,
793    ) -> ExecResult<SceneGraph> {
794        // TODO: Check version.
795        #[cfg(not(target_arch = "wasm32"))]
796        let _ = sketch;
797        #[cfg(target_arch = "wasm32")]
798        if self.scene_graph.sketch_mode != Some(sketch) {
799            web_sys::console::warn_1(
800                &format!(
801                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
802                    &self.scene_graph.sketch_mode
803                )
804                .into(),
805            );
806        }
807        self.scene_graph.sketch_mode = None;
808
809        // Execute.
810        let outcome = ctx.run_with_caching(self.program.clone()).await?;
811
812        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
813        self.update_state_after_exec(outcome, false);
814
815        Ok(self.scene_graph.clone())
816    }
817
818    async fn delete_sketch(
819        &mut self,
820        ctx: &ExecutorContext,
821        _version: Version,
822        sketch: ObjectId,
823    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
824        // TODO: Check version.
825
826        let mut new_ast = self.program.ast.clone();
827
828        // Look up existing sketch.
829        let sketch_id = sketch;
830        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
831            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
832        })?;
833        let ObjectKind::Sketch(_) = &sketch_object.kind else {
834            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
835                "Object is not a sketch, it is {}",
836                sketch_object.kind.human_friendly_kind_with_article(),
837            ))));
838        };
839
840        // Modify the AST to remove the sketch.
841        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
842            .map_err(KclErrorWithOutputs::no_outputs)?;
843
844        self.execute_after_delete_sketch(ctx, &mut new_ast).await
845    }
846
847    async fn add_segment(
848        &mut self,
849        ctx: &ExecutorContext,
850        _version: Version,
851        sketch: ObjectId,
852        segment: SegmentCtor,
853        _label: Option<String>,
854    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
855        // TODO: Check version.
856        match segment {
857            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
858            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
859            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
860            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
861        }
862    }
863
864    async fn edit_segments(
865        &mut self,
866        ctx: &ExecutorContext,
867        _version: Version,
868        sketch: ObjectId,
869        segments: Vec<ExistingSegmentCtor>,
870    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
871        // TODO: Check version.
872        let sketch_var_update_mode = self
873            .next_sketch_var_update_mode
874            .take()
875            .unwrap_or(SketchVarUpdateMode::CommitSolvedVars);
876        let sketch_block_ref =
877            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
878
879        let mut new_ast = self.program.ast.clone();
880        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
881
882        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
883        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
884        for segment in &segments {
885            segment_ids_edited.insert(segment.id);
886        }
887
888        // Preprocess segments into a final_edits vector to handle if segments contains:
889        // - edit start point of line1 (as SegmentCtor::Point)
890        // - edit end point of line1 (as SegmentCtor::Point)
891        //
892        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
893        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
894        //
895        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
896        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
897        // so the above example would result in a single line1 edit:
898        // - the first start point edit creates a new line edit entry in final_edits
899        // - the second end point edit finds this line edit and mutates the end position only.
900        //
901        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
902        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
903
904        for segment in segments {
905            let segment_id = segment.id;
906            match segment.ctor {
907                SegmentCtor::Point(ctor) => {
908                    // Find the owner, if any (point -> line / arc)
909                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
910                        && let ObjectKind::Segment { segment } = &segment_object.kind
911                        && let Segment::Point(point) = segment
912                        && let Some(owner_id) = point.owner
913                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
914                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
915                    {
916                        match owner_segment {
917                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
918                                if let Some(existing) = final_edits.get_mut(&owner_id) {
919                                    let SegmentCtor::Line(line_ctor) = existing else {
920                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
921                                            "Internal: Expected line ctor for owner, but found {}",
922                                            existing.human_friendly_kind_with_article()
923                                        ))));
924                                    };
925                                    // Line owner is already in final_edits -> apply this point edit
926                                    if line.start == segment_id {
927                                        line_ctor.start = ctor.position;
928                                    } else {
929                                        line_ctor.end = ctor.position;
930                                    }
931                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
932                                    // Line owner is not in final_edits yet -> create it
933                                    let mut line_ctor = line_ctor.clone();
934                                    if line.start == segment_id {
935                                        line_ctor.start = ctor.position;
936                                    } else {
937                                        line_ctor.end = ctor.position;
938                                    }
939                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
940                                } else {
941                                    // This should never run..
942                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
943                                        "Internal: Line does not have line ctor, but found {}",
944                                        line.ctor.human_friendly_kind_with_article()
945                                    ))));
946                                }
947                                continue;
948                            }
949                            Segment::Arc(arc)
950                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
951                            {
952                                if let Some(existing) = final_edits.get_mut(&owner_id) {
953                                    let SegmentCtor::Arc(arc_ctor) = existing else {
954                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
955                                            "Internal: Expected arc ctor for owner, but found {}",
956                                            existing.human_friendly_kind_with_article()
957                                        ))));
958                                    };
959                                    if arc.start == segment_id {
960                                        arc_ctor.start = ctor.position;
961                                    } else if arc.end == segment_id {
962                                        arc_ctor.end = ctor.position;
963                                    } else {
964                                        arc_ctor.center = ctor.position;
965                                    }
966                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
967                                    let mut arc_ctor = arc_ctor.clone();
968                                    if arc.start == segment_id {
969                                        arc_ctor.start = ctor.position;
970                                    } else if arc.end == segment_id {
971                                        arc_ctor.end = ctor.position;
972                                    } else {
973                                        arc_ctor.center = ctor.position;
974                                    }
975                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
976                                } else {
977                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
978                                        "Internal: Arc does not have arc ctor, but found {}",
979                                        arc.ctor.human_friendly_kind_with_article()
980                                    ))));
981                                }
982                                continue;
983                            }
984                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
985                                if let Some(existing) = final_edits.get_mut(&owner_id) {
986                                    let SegmentCtor::Circle(circle_ctor) = existing else {
987                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
988                                            "Internal: Expected circle ctor for owner, but found {}",
989                                            existing.human_friendly_kind_with_article()
990                                        ))));
991                                    };
992                                    if circle.start == segment_id {
993                                        circle_ctor.start = ctor.position;
994                                    } else {
995                                        circle_ctor.center = ctor.position;
996                                    }
997                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
998                                    let mut circle_ctor = circle_ctor.clone();
999                                    if circle.start == segment_id {
1000                                        circle_ctor.start = ctor.position;
1001                                    } else {
1002                                        circle_ctor.center = ctor.position;
1003                                    }
1004                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1005                                } else {
1006                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1007                                        "Internal: Circle does not have circle ctor, but found {}",
1008                                        circle.ctor.human_friendly_kind_with_article()
1009                                    ))));
1010                                }
1011                                continue;
1012                            }
1013                            _ => {}
1014                        }
1015                    }
1016
1017                    // No owner, it's an individual point
1018                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1019                }
1020                SegmentCtor::Line(ctor) => {
1021                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1022                }
1023                SegmentCtor::Arc(ctor) => {
1024                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1025                }
1026                SegmentCtor::Circle(ctor) => {
1027                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1028                }
1029            }
1030        }
1031
1032        for (segment_id, ctor) in final_edits {
1033            match ctor {
1034                SegmentCtor::Point(ctor) => self
1035                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1036                    .map_err(KclErrorWithOutputs::no_outputs)?,
1037                SegmentCtor::Line(ctor) => self
1038                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1039                    .map_err(KclErrorWithOutputs::no_outputs)?,
1040                SegmentCtor::Arc(ctor) => self
1041                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1042                    .map_err(KclErrorWithOutputs::no_outputs)?,
1043                SegmentCtor::Circle(ctor) => self
1044                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1045                    .map_err(KclErrorWithOutputs::no_outputs)?,
1046            }
1047        }
1048        self.execute_after_edit(
1049            ctx,
1050            sketch,
1051            sketch_block_ref,
1052            ExecuteAfterEditOptions {
1053                segment_ids_edited,
1054                edit_kind: EditDeleteKind::Edit,
1055                sketch_var_update_mode,
1056            },
1057            &mut new_ast,
1058        )
1059        .await
1060    }
1061
1062    async fn delete_objects(
1063        &mut self,
1064        ctx: &ExecutorContext,
1065        _version: Version,
1066        sketch: ObjectId,
1067        constraint_ids: Vec<ObjectId>,
1068        segment_ids: Vec<ObjectId>,
1069    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1070        // TODO: Check version.
1071        let sketch_block_ref =
1072            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1073
1074        // Deduplicate IDs.
1075        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1076        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1077
1078        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1079        // also delete the point, as well as other points that are owned by the owner.
1080        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1081
1082        for segment_id in segment_ids_set.iter().copied() {
1083            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1084                && let ObjectKind::Segment { segment } = &segment_object.kind
1085                && let Segment::Point(point) = segment
1086                && let Some(owner_id) = point.owner
1087                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1088                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1089                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
1090            {
1091                // segment is owned -> delete the owner
1092                resolved_segment_ids_to_delete.insert(owner_id);
1093            } else {
1094                // segment is not owned by anything -> can be deleted
1095                resolved_segment_ids_to_delete.insert(segment_id);
1096            }
1097        }
1098        let referenced_constraint_ids = self
1099            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1100            .map_err(KclErrorWithOutputs::no_outputs)?;
1101
1102        let mut new_ast = self.program.ast.clone();
1103
1104        for constraint_id in referenced_constraint_ids {
1105            if constraint_ids_set.contains(&constraint_id) {
1106                continue;
1107            }
1108
1109            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1110                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1111            })?;
1112            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1113                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                    "Object is not a constraint, it is {}",
1115                    constraint_object.kind.human_friendly_kind_with_article()
1116                ))));
1117            };
1118
1119            match constraint {
1120                Constraint::Coincident(coincident) => {
1121                    let remaining_segments =
1122                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1123
1124                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1125                    if remaining_segments.len() >= 2 {
1126                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1127                            .map_err(KclErrorWithOutputs::no_outputs)?;
1128                    } else {
1129                        constraint_ids_set.insert(constraint_id);
1130                    }
1131                }
1132                Constraint::EqualRadius(equal_radius) => {
1133                    let remaining_input = equal_radius
1134                        .input
1135                        .iter()
1136                        .copied()
1137                        .filter(|segment_id| {
1138                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1139                        })
1140                        .collect::<Vec<_>>();
1141
1142                    if remaining_input.len() >= 2 {
1143                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1144                            .map_err(KclErrorWithOutputs::no_outputs)?;
1145                    } else {
1146                        constraint_ids_set.insert(constraint_id);
1147                    }
1148                }
1149                Constraint::LinesEqualLength(lines_equal_length) => {
1150                    let remaining_lines = lines_equal_length
1151                        .lines
1152                        .iter()
1153                        .copied()
1154                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1155                        .collect::<Vec<_>>();
1156
1157                    // Equal length constraint is only valid with at least 2 lines
1158                    if remaining_lines.len() >= 2 {
1159                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1160                            .map_err(KclErrorWithOutputs::no_outputs)?;
1161                    } else {
1162                        constraint_ids_set.insert(constraint_id);
1163                    }
1164                }
1165                Constraint::Parallel(parallel) => {
1166                    let remaining_lines = parallel
1167                        .lines
1168                        .iter()
1169                        .copied()
1170                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1171                        .collect::<Vec<_>>();
1172
1173                    if remaining_lines.len() >= 2 {
1174                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1175                            .map_err(KclErrorWithOutputs::no_outputs)?;
1176                    } else {
1177                        constraint_ids_set.insert(constraint_id);
1178                    }
1179                }
1180                Constraint::Horizontal(Horizontal::Points { points }) => {
1181                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1182
1183                    if remaining_points.len() >= 2 {
1184                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1185                            .map_err(KclErrorWithOutputs::no_outputs)?;
1186                    } else {
1187                        constraint_ids_set.insert(constraint_id);
1188                    }
1189                }
1190                Constraint::Vertical(Vertical::Points { points }) => {
1191                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1192
1193                    if remaining_points.len() >= 2 {
1194                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1195                            .map_err(KclErrorWithOutputs::no_outputs)?;
1196                    } else {
1197                        constraint_ids_set.insert(constraint_id);
1198                    }
1199                }
1200                Constraint::Fixed(fixed) => {
1201                    if fixed.points.iter().any(|fixed_point| {
1202                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1203                    }) {
1204                        constraint_ids_set.insert(constraint_id);
1205                    }
1206                }
1207                _ => {
1208                    // All other constraint types: if referenced by a segment -> delete the constraint
1209                    constraint_ids_set.insert(constraint_id);
1210                }
1211            }
1212        }
1213
1214        for constraint_id in constraint_ids_set {
1215            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1216                .map_err(KclErrorWithOutputs::no_outputs)?;
1217        }
1218        for segment_id in resolved_segment_ids_to_delete {
1219            self.delete_segment(&mut new_ast, sketch, segment_id)
1220                .map_err(KclErrorWithOutputs::no_outputs)?;
1221        }
1222
1223        self.execute_after_edit(
1224            ctx,
1225            sketch,
1226            sketch_block_ref,
1227            ExecuteAfterEditOptions {
1228                segment_ids_edited: Default::default(),
1229                edit_kind: EditDeleteKind::DeleteNonSketch,
1230                sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1231            },
1232            &mut new_ast,
1233        )
1234        .await
1235    }
1236
1237    async fn add_constraint(
1238        &mut self,
1239        ctx: &ExecutorContext,
1240        _version: Version,
1241        sketch: ObjectId,
1242        constraint: Constraint,
1243    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1244        // TODO: Check version.
1245
1246        // Save the original state as a backup - we'll restore it if anything fails
1247        let original_program = self.program.clone();
1248        let original_scene_graph = self.scene_graph.clone();
1249
1250        let mut new_ast = self.program.ast.clone();
1251        let sketch_block_ref = match constraint {
1252            Constraint::Coincident(coincident) => self
1253                .add_coincident(sketch, coincident, &mut new_ast)
1254                .await
1255                .map_err(KclErrorWithOutputs::no_outputs)?,
1256            Constraint::Distance(distance) => self
1257                .add_distance(sketch, distance, &mut new_ast)
1258                .await
1259                .map_err(KclErrorWithOutputs::no_outputs)?,
1260            Constraint::EqualRadius(equal_radius) => self
1261                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1262                .await
1263                .map_err(KclErrorWithOutputs::no_outputs)?,
1264            Constraint::Fixed(fixed) => self
1265                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1266                .await
1267                .map_err(KclErrorWithOutputs::no_outputs)?,
1268            Constraint::HorizontalDistance(distance) => self
1269                .add_horizontal_distance(sketch, distance, &mut new_ast)
1270                .await
1271                .map_err(KclErrorWithOutputs::no_outputs)?,
1272            Constraint::VerticalDistance(distance) => self
1273                .add_vertical_distance(sketch, distance, &mut new_ast)
1274                .await
1275                .map_err(KclErrorWithOutputs::no_outputs)?,
1276            Constraint::Horizontal(horizontal) => self
1277                .add_horizontal(sketch, horizontal, &mut new_ast)
1278                .await
1279                .map_err(KclErrorWithOutputs::no_outputs)?,
1280            Constraint::LinesEqualLength(lines_equal_length) => self
1281                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1282                .await
1283                .map_err(KclErrorWithOutputs::no_outputs)?,
1284            Constraint::Midpoint(midpoint) => self
1285                .add_midpoint(sketch, midpoint, &mut new_ast)
1286                .await
1287                .map_err(KclErrorWithOutputs::no_outputs)?,
1288            Constraint::Parallel(parallel) => self
1289                .add_parallel(sketch, parallel, &mut new_ast)
1290                .await
1291                .map_err(KclErrorWithOutputs::no_outputs)?,
1292            Constraint::Perpendicular(perpendicular) => self
1293                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1294                .await
1295                .map_err(KclErrorWithOutputs::no_outputs)?,
1296            Constraint::Radius(radius) => self
1297                .add_radius(sketch, radius, &mut new_ast)
1298                .await
1299                .map_err(KclErrorWithOutputs::no_outputs)?,
1300            Constraint::Diameter(diameter) => self
1301                .add_diameter(sketch, diameter, &mut new_ast)
1302                .await
1303                .map_err(KclErrorWithOutputs::no_outputs)?,
1304            Constraint::Symmetric(symmetric) => self
1305                .add_symmetric(sketch, symmetric, &mut new_ast)
1306                .await
1307                .map_err(KclErrorWithOutputs::no_outputs)?,
1308            Constraint::Vertical(vertical) => self
1309                .add_vertical(sketch, vertical, &mut new_ast)
1310                .await
1311                .map_err(KclErrorWithOutputs::no_outputs)?,
1312            Constraint::Angle(lines_at_angle) => self
1313                .add_angle(sketch, lines_at_angle, &mut new_ast)
1314                .await
1315                .map_err(KclErrorWithOutputs::no_outputs)?,
1316            Constraint::Tangent(tangent) => self
1317                .add_tangent(sketch, tangent, &mut new_ast)
1318                .await
1319                .map_err(KclErrorWithOutputs::no_outputs)?,
1320        };
1321
1322        let result = self
1323            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1324            .await;
1325
1326        // If execution failed, restore the original state to prevent corruption
1327        if result.is_err() {
1328            self.program = original_program;
1329            self.scene_graph = original_scene_graph;
1330        }
1331
1332        result
1333    }
1334
1335    async fn chain_segment(
1336        &mut self,
1337        ctx: &ExecutorContext,
1338        version: Version,
1339        sketch: ObjectId,
1340        previous_segment_end_point_id: ObjectId,
1341        segment: SegmentCtor,
1342        _label: Option<String>,
1343    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1344        // TODO: Check version.
1345
1346        // First, add the segment (line) to get its start point ID
1347        let SegmentCtor::Line(line_ctor) = segment else {
1348            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1349                "chain_segment currently only supports Line segments, got {}",
1350                segment.human_friendly_kind_with_article(),
1351            ))));
1352        };
1353
1354        // Add the line segment first - this updates self.program and self.scene_graph
1355        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1356
1357        // Find the new line's start point ID from the updated scene graph
1358        // add_line updates self.scene_graph, so we can use that
1359        let new_line_id = first_scene_delta
1360            .new_objects
1361            .iter()
1362            .find(|&obj_id| {
1363                let obj = self.scene_graph.objects.get(obj_id.0);
1364                if let Some(obj) = obj {
1365                    matches!(
1366                        &obj.kind,
1367                        ObjectKind::Segment {
1368                            segment: Segment::Line(_)
1369                        }
1370                    )
1371                } else {
1372                    false
1373                }
1374            })
1375            .ok_or_else(|| {
1376                KclErrorWithOutputs::no_outputs(KclError::refactor(
1377                    "Failed to find new line segment in scene graph".to_string(),
1378                ))
1379            })?;
1380
1381        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1382            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1383                "New line object not found: {new_line_id:?}"
1384            )))
1385        })?;
1386
1387        let ObjectKind::Segment {
1388            segment: new_line_segment,
1389        } = &new_line_obj.kind
1390        else {
1391            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1392                "Object is not a segment: {new_line_obj:?}"
1393            ))));
1394        };
1395
1396        let Segment::Line(new_line) = new_line_segment else {
1397            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1398                "Segment is not a line: {new_line_segment:?}"
1399            ))));
1400        };
1401
1402        let new_line_start_point_id = new_line.start;
1403
1404        // Now add the coincident constraint between the previous end point and the new line's start point.
1405        let coincident = Coincident {
1406            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1407        };
1408
1409        let (final_src_delta, final_scene_delta) = self
1410            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1411            .await?;
1412
1413        // Combine new objects from the line addition and the constraint addition.
1414        // Both add_line and add_constraint now populate new_objects correctly.
1415        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1416        combined_new_objects.extend(final_scene_delta.new_objects);
1417
1418        let scene_graph_delta = SceneGraphDelta {
1419            new_graph: self.scene_graph.clone(),
1420            invalidates_ids: false,
1421            new_objects: combined_new_objects,
1422            exec_outcome: final_scene_delta.exec_outcome,
1423        };
1424
1425        Ok((final_src_delta, scene_graph_delta))
1426    }
1427
1428    async fn edit_constraint(
1429        &mut self,
1430        ctx: &ExecutorContext,
1431        _version: Version,
1432        sketch: ObjectId,
1433        constraint_id: ObjectId,
1434        value_expression: String,
1435    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1436        // TODO: Check version.
1437        let sketch_block_ref =
1438            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1439
1440        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1441            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1442        })?;
1443        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1444            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1445                "Object is not a constraint: {constraint_id:?}"
1446            ))));
1447        }
1448
1449        let mut new_ast = self.program.ast.clone();
1450
1451        // Parse the expression string into an AST node.
1452        let (parsed, errors) = Program::parse(&value_expression)
1453            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1454        if !errors.is_empty() {
1455            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1456                "Error parsing value expression: {errors:?}"
1457            ))));
1458        }
1459        let mut parsed = parsed.ok_or_else(|| {
1460            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1461        })?;
1462        if parsed.ast.body.is_empty() {
1463            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1464                "Empty value expression".to_string(),
1465            )));
1466        }
1467        let first = parsed.ast.body.remove(0);
1468        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1469            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1470                "Value expression must be a simple expression".to_string(),
1471            )));
1472        };
1473
1474        let new_value: ast::BinaryPart = expr_stmt
1475            .inner
1476            .expression
1477            .try_into()
1478            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1479
1480        self.mutate_ast(
1481            &mut new_ast,
1482            constraint_id,
1483            AstMutateCommand::EditConstraintValue { value: new_value },
1484        )
1485        .map_err(KclErrorWithOutputs::no_outputs)?;
1486
1487        self.execute_after_edit(
1488            ctx,
1489            sketch,
1490            sketch_block_ref,
1491            ExecuteAfterEditOptions {
1492                segment_ids_edited: Default::default(),
1493                edit_kind: EditDeleteKind::Edit,
1494                sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1495            },
1496            &mut new_ast,
1497        )
1498        .await
1499    }
1500
1501    async fn edit_distance_constraint_label_position(
1502        &mut self,
1503        ctx: &ExecutorContext,
1504        _version: Version,
1505        sketch: ObjectId,
1506        constraint_id: ObjectId,
1507        label_position: Point2d<Number>,
1508        anchor_segment_ids: Vec<ObjectId>,
1509    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1510        // TODO: Check version.
1511        let sketch_block_ref =
1512            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1513
1514        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1515            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1516        })?;
1517        if !matches!(
1518            &object.kind,
1519            ObjectKind::Constraint {
1520                constraint: Constraint::Distance(_)
1521                    | Constraint::HorizontalDistance(_)
1522                    | Constraint::VerticalDistance(_)
1523                    | Constraint::Radius(_)
1524                    | Constraint::Diameter(_),
1525            }
1526        ) {
1527            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1528                "Object does not support labelPosition: {constraint_id:?}"
1529            ))));
1530        }
1531
1532        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1533            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1534                "Could not convert label position to AST: {err}"
1535            )))
1536        })?;
1537        let mut new_ast = self.program.ast.clone();
1538        self.mutate_ast(
1539            &mut new_ast,
1540            constraint_id,
1541            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1542        )
1543        .map_err(KclErrorWithOutputs::no_outputs)?;
1544
1545        self.execute_after_edit(
1546            ctx,
1547            sketch,
1548            sketch_block_ref,
1549            ExecuteAfterEditOptions {
1550                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1551                edit_kind: EditDeleteKind::Edit,
1552                sketch_var_update_mode: SketchVarUpdateMode::WarmStartOnly,
1553            },
1554            &mut new_ast,
1555        )
1556        .await
1557    }
1558
1559    /// Splitting a segment means creating a new segment, editing the old one, and then
1560    /// migrating a bunch of the constraints from the original segment to the new one
1561    /// (i.e. deleting them and re-adding them on the other segment).
1562    ///
1563    /// To keep this efficient we require as few executions as possible: we create the
1564    /// new segment first (to get its id), then do all edits and new constraints, and
1565    /// do all deletes at the end (since deletes invalidate ids).
1566    async fn batch_split_segment_operations(
1567        &mut self,
1568        ctx: &ExecutorContext,
1569        _version: Version,
1570        sketch: ObjectId,
1571        edit_segments: Vec<ExistingSegmentCtor>,
1572        add_constraints: Vec<Constraint>,
1573        delete_constraint_ids: Vec<ObjectId>,
1574        _new_segment_info: sketch::NewSegmentInfo,
1575    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1576        // TODO: Check version.
1577        let sketch_block_ref =
1578            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1579
1580        let mut new_ast = self.program.ast.clone();
1581        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1582
1583        // Step 1: Edit segments
1584        for segment in edit_segments {
1585            segment_ids_edited.insert(segment.id);
1586            match segment.ctor {
1587                SegmentCtor::Point(ctor) => self
1588                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1589                    .map_err(KclErrorWithOutputs::no_outputs)?,
1590                SegmentCtor::Line(ctor) => self
1591                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1592                    .map_err(KclErrorWithOutputs::no_outputs)?,
1593                SegmentCtor::Arc(ctor) => self
1594                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1595                    .map_err(KclErrorWithOutputs::no_outputs)?,
1596                SegmentCtor::Circle(ctor) => self
1597                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1598                    .map_err(KclErrorWithOutputs::no_outputs)?,
1599            }
1600        }
1601
1602        // Step 2: Add all constraints
1603        for constraint in add_constraints {
1604            match constraint {
1605                Constraint::Coincident(coincident) => {
1606                    self.add_coincident(sketch, coincident, &mut new_ast)
1607                        .await
1608                        .map_err(KclErrorWithOutputs::no_outputs)?;
1609                }
1610                Constraint::Distance(distance) => {
1611                    self.add_distance(sketch, distance, &mut new_ast)
1612                        .await
1613                        .map_err(KclErrorWithOutputs::no_outputs)?;
1614                }
1615                Constraint::EqualRadius(equal_radius) => {
1616                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1617                        .await
1618                        .map_err(KclErrorWithOutputs::no_outputs)?;
1619                }
1620                Constraint::Fixed(fixed) => {
1621                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1622                        .await
1623                        .map_err(KclErrorWithOutputs::no_outputs)?;
1624                }
1625                Constraint::HorizontalDistance(distance) => {
1626                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1627                        .await
1628                        .map_err(KclErrorWithOutputs::no_outputs)?;
1629                }
1630                Constraint::VerticalDistance(distance) => {
1631                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1632                        .await
1633                        .map_err(KclErrorWithOutputs::no_outputs)?;
1634                }
1635                Constraint::Horizontal(horizontal) => {
1636                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1637                        .await
1638                        .map_err(KclErrorWithOutputs::no_outputs)?;
1639                }
1640                Constraint::LinesEqualLength(lines_equal_length) => {
1641                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1642                        .await
1643                        .map_err(KclErrorWithOutputs::no_outputs)?;
1644                }
1645                Constraint::Midpoint(midpoint) => {
1646                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1647                        .await
1648                        .map_err(KclErrorWithOutputs::no_outputs)?;
1649                }
1650                Constraint::Parallel(parallel) => {
1651                    self.add_parallel(sketch, parallel, &mut new_ast)
1652                        .await
1653                        .map_err(KclErrorWithOutputs::no_outputs)?;
1654                }
1655                Constraint::Perpendicular(perpendicular) => {
1656                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1657                        .await
1658                        .map_err(KclErrorWithOutputs::no_outputs)?;
1659                }
1660                Constraint::Vertical(vertical) => {
1661                    self.add_vertical(sketch, vertical, &mut new_ast)
1662                        .await
1663                        .map_err(KclErrorWithOutputs::no_outputs)?;
1664                }
1665                Constraint::Diameter(diameter) => {
1666                    self.add_diameter(sketch, diameter, &mut new_ast)
1667                        .await
1668                        .map_err(KclErrorWithOutputs::no_outputs)?;
1669                }
1670                Constraint::Radius(radius) => {
1671                    self.add_radius(sketch, radius, &mut new_ast)
1672                        .await
1673                        .map_err(KclErrorWithOutputs::no_outputs)?;
1674                }
1675                Constraint::Symmetric(symmetric) => {
1676                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1677                        .await
1678                        .map_err(KclErrorWithOutputs::no_outputs)?;
1679                }
1680                Constraint::Angle(angle) => {
1681                    self.add_angle(sketch, angle, &mut new_ast)
1682                        .await
1683                        .map_err(KclErrorWithOutputs::no_outputs)?;
1684                }
1685                Constraint::Tangent(tangent) => {
1686                    self.add_tangent(sketch, tangent, &mut new_ast)
1687                        .await
1688                        .map_err(KclErrorWithOutputs::no_outputs)?;
1689                }
1690            }
1691        }
1692
1693        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1694        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1695
1696        let has_constraint_deletions = !constraint_ids_set.is_empty();
1697        for constraint_id in constraint_ids_set {
1698            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1699                .map_err(KclErrorWithOutputs::no_outputs)?;
1700        }
1701
1702        // Step 4: Execute once at the end
1703        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1704        // But we'll manually set invalidates_ids: true if we deleted constraints
1705        let (source_delta, mut scene_graph_delta) = self
1706            .execute_after_edit(
1707                ctx,
1708                sketch,
1709                sketch_block_ref,
1710                ExecuteAfterEditOptions {
1711                    segment_ids_edited,
1712                    edit_kind: EditDeleteKind::Edit,
1713                    sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1714                },
1715                &mut new_ast,
1716            )
1717            .await?;
1718
1719        // If we deleted constraints, set invalidates_ids: true
1720        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1721        if has_constraint_deletions {
1722            scene_graph_delta.invalidates_ids = true;
1723        }
1724
1725        Ok((source_delta, scene_graph_delta))
1726    }
1727
1728    async fn batch_tail_cut_operations(
1729        &mut self,
1730        ctx: &ExecutorContext,
1731        _version: Version,
1732        sketch: ObjectId,
1733        edit_segments: Vec<ExistingSegmentCtor>,
1734        add_constraints: Vec<Constraint>,
1735        delete_constraint_ids: Vec<ObjectId>,
1736        additional_edited_segment_ids: Vec<ObjectId>,
1737    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1738        let sketch_block_ref =
1739            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1740
1741        let mut new_ast = self.program.ast.clone();
1742        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1743
1744        // Step 1: Edit segments (usually a single segment for tail cut)
1745        for segment in edit_segments {
1746            segment_ids_edited.insert(segment.id);
1747            match segment.ctor {
1748                SegmentCtor::Point(ctor) => self
1749                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1750                    .map_err(KclErrorWithOutputs::no_outputs)?,
1751                SegmentCtor::Line(ctor) => self
1752                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1753                    .map_err(KclErrorWithOutputs::no_outputs)?,
1754                SegmentCtor::Arc(ctor) => self
1755                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1756                    .map_err(KclErrorWithOutputs::no_outputs)?,
1757                SegmentCtor::Circle(ctor) => self
1758                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1759                    .map_err(KclErrorWithOutputs::no_outputs)?,
1760            }
1761        }
1762
1763        segment_ids_edited.extend(additional_edited_segment_ids);
1764
1765        // Step 2: Add coincident constraints
1766        for constraint in add_constraints {
1767            match constraint {
1768                Constraint::Coincident(coincident) => {
1769                    self.add_coincident(sketch, coincident, &mut new_ast)
1770                        .await
1771                        .map_err(KclErrorWithOutputs::no_outputs)?;
1772                }
1773                other => {
1774                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1775                        "unsupported constraint in tail cut batch: {other:?}"
1776                    ))));
1777                }
1778            }
1779        }
1780
1781        // Step 3: Delete constraints (if any)
1782        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1783
1784        let has_constraint_deletions = !constraint_ids_set.is_empty();
1785        for constraint_id in constraint_ids_set {
1786            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1787                .map_err(KclErrorWithOutputs::no_outputs)?;
1788        }
1789
1790        // Step 4: Single execute_after_edit
1791        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1792        // But we'll manually set invalidates_ids: true if we deleted constraints
1793        let (source_delta, mut scene_graph_delta) = self
1794            .execute_after_edit(
1795                ctx,
1796                sketch,
1797                sketch_block_ref,
1798                ExecuteAfterEditOptions {
1799                    segment_ids_edited,
1800                    edit_kind: EditDeleteKind::Edit,
1801                    sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1802                },
1803                &mut new_ast,
1804            )
1805            .await?;
1806
1807        // If we deleted constraints, set invalidates_ids: true
1808        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1809        if has_constraint_deletions {
1810            scene_graph_delta.invalidates_ids = true;
1811        }
1812
1813        Ok((source_delta, scene_graph_delta))
1814    }
1815}
1816
1817impl FrontendState {
1818    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1819        self.program = program.clone();
1820        self.clear_sketch_var_warm_starts();
1821
1822        // Execute so that the objects are updated and available for the next
1823        // API call.
1824        // This always uses engine execution (not mock) so that things are cached.
1825        // Engine execution now runs freedom analysis automatically.
1826        // Keep existing checkpoints alive here. History may still reference
1827        // older committed sketch states across a direct-edit boundary, and a
1828        // checkpoint restore is a full state replacement anyway. We append a
1829        // fresh baseline checkpoint after the full execution below.
1830        // Clear the freedom cache since IDs might have changed after direct editing
1831        // and we're about to run freedom analysis which will repopulate it.
1832        self.point_freedom_cache.clear();
1833        match ctx.run_with_caching(program).await {
1834            Ok(outcome) => {
1835                let outcome = self.update_state_after_exec(outcome, true);
1836                self.update_single_sketch_var_warm_starts(&outcome);
1837                let checkpoint_id = self
1838                    .create_sketch_checkpoint(outcome.clone())
1839                    .await
1840                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1841                Ok(SetProgramOutcome::Success {
1842                    scene_graph: Box::new(self.scene_graph.clone()),
1843                    exec_outcome: Box::new(outcome),
1844                    checkpoint_id: Some(checkpoint_id),
1845                })
1846            }
1847            Err(mut err) => {
1848                // Don't return an error just because execution failed. Instead,
1849                // update state as much as possible.
1850                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1851                self.update_state_after_exec(outcome, true);
1852                err.scene_graph = Some(self.scene_graph.clone());
1853                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1854            }
1855        }
1856    }
1857
1858    /// Decorate engine execution such that our state is updated and the scene
1859    /// graph is added to the return.
1860    pub async fn engine_execute(
1861        &mut self,
1862        ctx: &ExecutorContext,
1863        program: Program,
1864    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1865        self.program = program.clone();
1866        self.clear_sketch_var_warm_starts();
1867
1868        // Engine execution now runs freedom analysis automatically. Clear the
1869        // freedom cache since IDs might have changed after direct editing, and
1870        // we're about to run freedom analysis which will repopulate it.
1871        self.point_freedom_cache.clear();
1872        match ctx.run_with_caching(program).await {
1873            Ok(outcome) => {
1874                let outcome = self.update_state_after_exec(outcome, true);
1875                self.update_single_sketch_var_warm_starts(&outcome);
1876                Ok(SceneGraphDelta {
1877                    new_graph: self.scene_graph.clone(),
1878                    exec_outcome: outcome,
1879                    // We don't know what the new objects are.
1880                    new_objects: Default::default(),
1881                    // We don't know if IDs were invalidated.
1882                    invalidates_ids: Default::default(),
1883                })
1884            }
1885            Err(mut err) => {
1886                // Update state as much as possible, even when there's an error.
1887                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1888                self.update_state_after_exec(outcome, true);
1889                err.scene_graph = Some(self.scene_graph.clone());
1890                Err(err)
1891            }
1892        }
1893    }
1894
1895    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1896        if matches!(err.error, KclError::EngineHangup { .. }) {
1897            // It's not ideal to special-case this, but this error is very
1898            // common during development, and it causes confusing downstream
1899            // errors that have nothing to do with the actual problem.
1900            return Err(err);
1901        }
1902
1903        let KclErrorWithOutputs {
1904            error,
1905            mut non_fatal,
1906            variables,
1907            operations,
1908            artifact_graph,
1909            scene_objects,
1910            source_range_to_object,
1911            var_solutions,
1912            filenames,
1913            default_planes,
1914            ..
1915        } = err;
1916
1917        if let Some(source_range) = error.source_ranges().first() {
1918            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1919        } else {
1920            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1921        }
1922
1923        Ok(ExecOutcome {
1924            variables,
1925            filenames,
1926            operations,
1927            artifact_graph,
1928            scene_objects,
1929            source_range_to_object,
1930            var_solutions,
1931            issues: non_fatal,
1932            default_planes,
1933        })
1934    }
1935
1936    async fn add_point(
1937        &mut self,
1938        ctx: &ExecutorContext,
1939        sketch: ObjectId,
1940        ctor: PointCtor,
1941    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1942        // Create updated KCL source from args.
1943        let at_ast = to_ast_point2d(&ctor.position)
1944            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1945        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1946            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1947            unlabeled: None,
1948            arguments: vec![ast::LabeledArg {
1949                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1950                arg: at_ast,
1951            }],
1952            digest: None,
1953            non_code_meta: Default::default(),
1954        })));
1955
1956        // Look up existing sketch.
1957        let sketch_id = sketch;
1958        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1959            #[cfg(target_arch = "wasm32")]
1960            web_sys::console::error_1(
1961                &format!(
1962                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1963                    &self.scene_graph.objects
1964                )
1965                .into(),
1966            );
1967            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1968        })?;
1969        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1970            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1971                "Object is not a sketch, it is {}",
1972                sketch_object.kind.human_friendly_kind_with_article(),
1973            ))));
1974        };
1975        // Add the point to the AST of the sketch block.
1976        let mut new_ast = self.program.ast.clone();
1977        let (sketch_block_ref, _) = self
1978            .mutate_ast(
1979                &mut new_ast,
1980                sketch_id,
1981                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1982            )
1983            .map_err(KclErrorWithOutputs::no_outputs)?;
1984        // Convert to string source to create real source ranges.
1985        let new_source = source_from_ast(&new_ast);
1986        // Parse the new KCL source.
1987        let (new_program, errors) = Program::parse(&new_source)
1988            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1989        if !errors.is_empty() {
1990            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1991                "Error parsing KCL source after adding point: {errors:?}"
1992            ))));
1993        }
1994        let Some(new_program) = new_program else {
1995            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1996                "No AST produced after adding point".to_string(),
1997            )));
1998        };
1999
2000        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2001            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2002                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2003            )))
2004        })?;
2005
2006        // Make sure to only set this if there are no errors.
2007        self.program = new_program.clone();
2008
2009        // Truncate after the sketch block for mock execution.
2010        let mut truncated_program = new_program;
2011        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2012            .map_err(KclErrorWithOutputs::no_outputs)?;
2013
2014        // Execute.
2015        let outcome = ctx
2016            .run_mock(
2017                &truncated_program,
2018                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2019            )
2020            .await?;
2021
2022        let new_object_ids = {
2023            let make_err =
2024                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2025            let segment_id = outcome
2026                .source_range_to_object
2027                .get(&point_node_ref.range)
2028                .copied()
2029                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2030            let segment_object = outcome
2031                .scene_objects
2032                .get(segment_id.0)
2033                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2034            let ObjectKind::Segment { segment } = &segment_object.kind else {
2035                return Err(make_err(format!(
2036                    "Object is not a segment, it is {}",
2037                    segment_object.kind.human_friendly_kind_with_article()
2038                )));
2039            };
2040            let Segment::Point(_) = segment else {
2041                return Err(make_err(format!(
2042                    "Segment is not a point, it is {}",
2043                    segment.human_friendly_kind_with_article()
2044                )));
2045            };
2046            vec![segment_id]
2047        };
2048        let src_delta = SourceDelta { text: new_source };
2049        // Uses .no_freedom_analysis() so freedom_analysis: false
2050        let outcome = self.update_state_after_exec(outcome, false);
2051        let scene_graph_delta = SceneGraphDelta {
2052            new_graph: self.scene_graph.clone(),
2053            invalidates_ids: false,
2054            new_objects: new_object_ids,
2055            exec_outcome: outcome,
2056        };
2057        Ok((src_delta, scene_graph_delta))
2058    }
2059
2060    async fn add_line(
2061        &mut self,
2062        ctx: &ExecutorContext,
2063        sketch: ObjectId,
2064        ctor: LineCtor,
2065    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2066        // Create updated KCL source from args.
2067        let start_ast = to_ast_point2d(&ctor.start)
2068            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2069        let end_ast = to_ast_point2d(&ctor.end)
2070            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2071        let mut arguments = vec![
2072            ast::LabeledArg {
2073                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2074                arg: start_ast,
2075            },
2076            ast::LabeledArg {
2077                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2078                arg: end_ast,
2079            },
2080        ];
2081        // Add construction kwarg if construction is Some(true)
2082        if ctor.construction == Some(true) {
2083            arguments.push(ast::LabeledArg {
2084                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2085                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2086                    value: ast::LiteralValue::Bool(true),
2087                    raw: "true".to_string(),
2088                    digest: None,
2089                }))),
2090            });
2091        }
2092        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2093            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2094            unlabeled: None,
2095            arguments,
2096            digest: None,
2097            non_code_meta: Default::default(),
2098        })));
2099
2100        // Look up existing sketch.
2101        let sketch_id = sketch;
2102        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2104        })?;
2105        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2106            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2107                "Object is not a sketch, it is {}",
2108                sketch_object.kind.human_friendly_kind_with_article(),
2109            ))));
2110        };
2111        // Add the line to the AST of the sketch block.
2112        let mut new_ast = self.program.ast.clone();
2113        let (sketch_block_ref, _) = self
2114            .mutate_ast(
2115                &mut new_ast,
2116                sketch_id,
2117                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2118            )
2119            .map_err(KclErrorWithOutputs::no_outputs)?;
2120        // Convert to string source to create real source ranges.
2121        let new_source = source_from_ast(&new_ast);
2122        // Parse the new KCL source.
2123        let (new_program, errors) = Program::parse(&new_source)
2124            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2125        if !errors.is_empty() {
2126            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2127                "Error parsing KCL source after adding line: {errors:?}"
2128            ))));
2129        }
2130        let Some(new_program) = new_program else {
2131            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2132                "No AST produced after adding line".to_string(),
2133            )));
2134        };
2135
2136        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2137            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2138                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2139            )))
2140        })?;
2141
2142        // Make sure to only set this if there are no errors.
2143        self.program = new_program.clone();
2144
2145        // Truncate after the sketch block for mock execution.
2146        let mut truncated_program = new_program;
2147        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2148            .map_err(KclErrorWithOutputs::no_outputs)?;
2149
2150        // Execute.
2151        let outcome = ctx
2152            .run_mock(
2153                &truncated_program,
2154                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2155            )
2156            .await?;
2157
2158        let new_object_ids = {
2159            let make_err =
2160                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2161            let segment_id = outcome
2162                .source_range_to_object
2163                .get(&line_node_ref.range)
2164                .copied()
2165                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2166            let segment_object = outcome
2167                .scene_object_by_id(segment_id)
2168                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169            let ObjectKind::Segment { segment } = &segment_object.kind else {
2170                return Err(make_err(format!(
2171                    "Object is not a segment, it is {}",
2172                    segment_object.kind.human_friendly_kind_with_article()
2173                )));
2174            };
2175            let Segment::Line(line) = segment else {
2176                return Err(make_err(format!(
2177                    "Segment is not a line, it is {}",
2178                    segment.human_friendly_kind_with_article()
2179                )));
2180            };
2181            vec![line.start, line.end, segment_id]
2182        };
2183        let src_delta = SourceDelta { text: new_source };
2184        // Uses .no_freedom_analysis() so freedom_analysis: false
2185        let outcome = self.update_state_after_exec(outcome, false);
2186        let scene_graph_delta = SceneGraphDelta {
2187            new_graph: self.scene_graph.clone(),
2188            invalidates_ids: false,
2189            new_objects: new_object_ids,
2190            exec_outcome: outcome,
2191        };
2192        Ok((src_delta, scene_graph_delta))
2193    }
2194
2195    async fn add_arc(
2196        &mut self,
2197        ctx: &ExecutorContext,
2198        sketch: ObjectId,
2199        ctor: ArcCtor,
2200    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201        // Create updated KCL source from args.
2202        let start_ast = to_ast_point2d(&ctor.start)
2203            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204        let end_ast = to_ast_point2d(&ctor.end)
2205            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206        let center_ast = to_ast_point2d(&ctor.center)
2207            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208        let mut arguments = vec![
2209            ast::LabeledArg {
2210                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2211                arg: start_ast,
2212            },
2213            ast::LabeledArg {
2214                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2215                arg: end_ast,
2216            },
2217            ast::LabeledArg {
2218                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2219                arg: center_ast,
2220            },
2221        ];
2222        // Add construction kwarg if construction is Some(true)
2223        if ctor.construction == Some(true) {
2224            arguments.push(ast::LabeledArg {
2225                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2226                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2227                    value: ast::LiteralValue::Bool(true),
2228                    raw: "true".to_string(),
2229                    digest: None,
2230                }))),
2231            });
2232        }
2233        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2234            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2235            unlabeled: None,
2236            arguments,
2237            digest: None,
2238            non_code_meta: Default::default(),
2239        })));
2240
2241        // Look up existing sketch.
2242        let sketch_id = sketch;
2243        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2244            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2245        })?;
2246        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2247            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2248                "Object is not a sketch, it is {}",
2249                sketch_object.kind.human_friendly_kind_with_article(),
2250            ))));
2251        };
2252        // Add the arc to the AST of the sketch block.
2253        let mut new_ast = self.program.ast.clone();
2254        let (sketch_block_ref, _) = self
2255            .mutate_ast(
2256                &mut new_ast,
2257                sketch_id,
2258                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2259            )
2260            .map_err(KclErrorWithOutputs::no_outputs)?;
2261        // Convert to string source to create real source ranges.
2262        let new_source = source_from_ast(&new_ast);
2263        // Parse the new KCL source.
2264        let (new_program, errors) = Program::parse(&new_source)
2265            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2266        if !errors.is_empty() {
2267            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268                "Error parsing KCL source after adding arc: {errors:?}"
2269            ))));
2270        }
2271        let Some(new_program) = new_program else {
2272            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2273                "No AST produced after adding arc".to_string(),
2274            )));
2275        };
2276
2277        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2278            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2279                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2280            )))
2281        })?;
2282
2283        // Make sure to only set this if there are no errors.
2284        self.program = new_program.clone();
2285
2286        // Truncate after the sketch block for mock execution.
2287        let mut truncated_program = new_program;
2288        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2289            .map_err(KclErrorWithOutputs::no_outputs)?;
2290
2291        // Execute.
2292        let outcome = ctx
2293            .run_mock(
2294                &truncated_program,
2295                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2296            )
2297            .await?;
2298
2299        let new_object_ids = {
2300            let make_err =
2301                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2302            let segment_id = outcome
2303                .source_range_to_object
2304                .get(&arc_node_ref.range)
2305                .copied()
2306                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2307            let segment_object = outcome
2308                .scene_objects
2309                .get(segment_id.0)
2310                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2311            let ObjectKind::Segment { segment } = &segment_object.kind else {
2312                return Err(make_err(format!(
2313                    "Object is not a segment, it is {}",
2314                    segment_object.kind.human_friendly_kind_with_article()
2315                )));
2316            };
2317            let Segment::Arc(arc) = segment else {
2318                return Err(make_err(format!(
2319                    "Segment is not an arc, it is {}",
2320                    segment.human_friendly_kind_with_article()
2321                )));
2322            };
2323            vec![arc.start, arc.end, arc.center, segment_id]
2324        };
2325        let src_delta = SourceDelta { text: new_source };
2326        // Uses .no_freedom_analysis() so freedom_analysis: false
2327        let outcome = self.update_state_after_exec(outcome, false);
2328        let scene_graph_delta = SceneGraphDelta {
2329            new_graph: self.scene_graph.clone(),
2330            invalidates_ids: false,
2331            new_objects: new_object_ids,
2332            exec_outcome: outcome,
2333        };
2334        Ok((src_delta, scene_graph_delta))
2335    }
2336
2337    async fn add_circle(
2338        &mut self,
2339        ctx: &ExecutorContext,
2340        sketch: ObjectId,
2341        ctor: CircleCtor,
2342    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2343        // Create updated KCL source from args.
2344        let start_ast = to_ast_point2d(&ctor.start)
2345            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2346        let center_ast = to_ast_point2d(&ctor.center)
2347            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2348        let mut arguments = vec![
2349            ast::LabeledArg {
2350                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2351                arg: start_ast,
2352            },
2353            ast::LabeledArg {
2354                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2355                arg: center_ast,
2356            },
2357        ];
2358        // Add construction kwarg if construction is Some(true)
2359        if ctor.construction == Some(true) {
2360            arguments.push(ast::LabeledArg {
2361                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2363                    value: ast::LiteralValue::Bool(true),
2364                    raw: "true".to_string(),
2365                    digest: None,
2366                }))),
2367            });
2368        }
2369        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2370            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2371            unlabeled: None,
2372            arguments,
2373            digest: None,
2374            non_code_meta: Default::default(),
2375        })));
2376
2377        // Look up existing sketch.
2378        let sketch_id = sketch;
2379        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381        })?;
2382        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384                "Object is not a sketch, it is {}",
2385                sketch_object.kind.human_friendly_kind_with_article(),
2386            ))));
2387        };
2388        // Add the circle to the AST of the sketch block.
2389        let mut new_ast = self.program.ast.clone();
2390        let (sketch_block_ref, _) = self
2391            .mutate_ast(
2392                &mut new_ast,
2393                sketch_id,
2394                AstMutateCommand::AddSketchBlockVarDecl {
2395                    prefix: CIRCLE_VARIABLE.to_owned(),
2396                    expr: circle_ast,
2397                },
2398            )
2399            .map_err(KclErrorWithOutputs::no_outputs)?;
2400        // Convert to string source to create real source ranges.
2401        let new_source = source_from_ast(&new_ast);
2402        // Parse the new KCL source.
2403        let (new_program, errors) = Program::parse(&new_source)
2404            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2405        if !errors.is_empty() {
2406            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2407                "Error parsing KCL source after adding circle: {errors:?}"
2408            ))));
2409        }
2410        let Some(new_program) = new_program else {
2411            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2412                "No AST produced after adding circle".to_string(),
2413            )));
2414        };
2415
2416        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2417            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2418                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2419            )))
2420        })?;
2421
2422        // Make sure to only set this if there are no errors.
2423        self.program = new_program.clone();
2424
2425        // Truncate after the sketch block for mock execution.
2426        let mut truncated_program = new_program;
2427        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2428            .map_err(KclErrorWithOutputs::no_outputs)?;
2429
2430        // Execute.
2431        let outcome = ctx
2432            .run_mock(
2433                &truncated_program,
2434                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2435            )
2436            .await?;
2437
2438        let new_object_ids = {
2439            let make_err =
2440                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2441            let segment_id = outcome
2442                .source_range_to_object
2443                .get(&circle_node_ref.range)
2444                .copied()
2445                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2446            let segment_object = outcome
2447                .scene_objects
2448                .get(segment_id.0)
2449                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2450            let ObjectKind::Segment { segment } = &segment_object.kind else {
2451                return Err(make_err(format!(
2452                    "Object is not a segment, it is {}",
2453                    segment_object.kind.human_friendly_kind_with_article()
2454                )));
2455            };
2456            let Segment::Circle(circle) = segment else {
2457                return Err(make_err(format!(
2458                    "Segment is not a circle, it is {}",
2459                    segment.human_friendly_kind_with_article()
2460                )));
2461            };
2462            vec![circle.start, circle.center, segment_id]
2463        };
2464        let src_delta = SourceDelta { text: new_source };
2465        // Uses .no_freedom_analysis() so freedom_analysis: false
2466        let outcome = self.update_state_after_exec(outcome, false);
2467        let scene_graph_delta = SceneGraphDelta {
2468            new_graph: self.scene_graph.clone(),
2469            invalidates_ids: false,
2470            new_objects: new_object_ids,
2471            exec_outcome: outcome,
2472        };
2473        Ok((src_delta, scene_graph_delta))
2474    }
2475
2476    fn edit_point(
2477        &mut self,
2478        new_ast: &mut ast::Node<ast::Program>,
2479        sketch: ObjectId,
2480        point: ObjectId,
2481        ctor: PointCtor,
2482    ) -> Result<(), KclError> {
2483        // Create updated KCL source from args.
2484        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2485
2486        // Look up existing sketch.
2487        let sketch_id = sketch;
2488        let sketch_object = self
2489            .scene_graph
2490            .objects
2491            .get(sketch_id.0)
2492            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2493        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2494            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2495        };
2496        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2497            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2498        })?;
2499        // Look up existing point.
2500        let point_id = point;
2501        let point_object = self
2502            .scene_graph
2503            .objects
2504            .get(point_id.0)
2505            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2506        let ObjectKind::Segment {
2507            segment: Segment::Point(point),
2508        } = &point_object.kind
2509        else {
2510            return Err(KclError::refactor(format!(
2511                "Object is not a point segment: {point_object:?}"
2512            )));
2513        };
2514
2515        // If the point is part of a line or arc, edit the line/arc instead.
2516        if let Some(owner_id) = point.owner {
2517            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2518                KclError::refactor(format!(
2519                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2520                ))
2521            })?;
2522            let ObjectKind::Segment { segment } = &owner_object.kind else {
2523                return Err(KclError::refactor(format!(
2524                    "Internal: Owner of point is not a segment, but found {}",
2525                    owner_object.kind.human_friendly_kind_with_article()
2526                )));
2527            };
2528
2529            // Handle Line owner
2530            if let Segment::Line(line) = segment {
2531                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2532                    return Err(KclError::refactor(format!(
2533                        "Internal: Owner of point does not have line ctor, but found {}",
2534                        line.ctor.human_friendly_kind_with_article()
2535                    )));
2536                };
2537                let mut line_ctor = line_ctor.clone();
2538                // Which end of the line is this point?
2539                if line.start == point_id {
2540                    line_ctor.start = ctor.position;
2541                } else if line.end == point_id {
2542                    line_ctor.end = ctor.position;
2543                } else {
2544                    return Err(KclError::refactor(format!(
2545                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2546                    )));
2547                }
2548                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2549            }
2550
2551            // Handle Arc owner
2552            if let Segment::Arc(arc) = segment {
2553                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2554                    return Err(KclError::refactor(format!(
2555                        "Internal: Owner of point does not have arc ctor, but found {}",
2556                        arc.ctor.human_friendly_kind_with_article()
2557                    )));
2558                };
2559                let mut arc_ctor = arc_ctor.clone();
2560                // Which point of the arc is this? (center, start, or end)
2561                if arc.center == point_id {
2562                    arc_ctor.center = ctor.position;
2563                } else if arc.start == point_id {
2564                    arc_ctor.start = ctor.position;
2565                } else if arc.end == point_id {
2566                    arc_ctor.end = ctor.position;
2567                } else {
2568                    return Err(KclError::refactor(format!(
2569                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2570                    )));
2571                }
2572                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2573            }
2574
2575            // Handle Circle owner
2576            if let Segment::Circle(circle) = segment {
2577                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2578                    return Err(KclError::refactor(format!(
2579                        "Internal: Owner of point does not have circle ctor, but found {}",
2580                        circle.ctor.human_friendly_kind_with_article()
2581                    )));
2582                };
2583                let mut circle_ctor = circle_ctor.clone();
2584                if circle.center == point_id {
2585                    circle_ctor.center = ctor.position;
2586                } else if circle.start == point_id {
2587                    circle_ctor.start = ctor.position;
2588                } else {
2589                    return Err(KclError::refactor(format!(
2590                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2591                    )));
2592                }
2593                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2594            }
2595
2596            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2597            // (fall through to the point editing logic below)
2598        }
2599
2600        // Modify the point AST.
2601        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2602        Ok(())
2603    }
2604
2605    fn edit_line(
2606        &mut self,
2607        new_ast: &mut ast::Node<ast::Program>,
2608        sketch: ObjectId,
2609        line: ObjectId,
2610        ctor: LineCtor,
2611    ) -> Result<(), KclError> {
2612        // Create updated KCL source from args.
2613        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2614        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2615
2616        // Look up existing sketch.
2617        let sketch_id = sketch;
2618        let sketch_object = self
2619            .scene_graph
2620            .objects
2621            .get(sketch_id.0)
2622            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2623        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2624            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2625        };
2626        sketch
2627            .segments
2628            .iter()
2629            .find(|o| **o == line)
2630            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2631        // Look up existing line.
2632        let line_id = line;
2633        let line_object = self
2634            .scene_graph
2635            .objects
2636            .get(line_id.0)
2637            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2638        let ObjectKind::Segment { .. } = &line_object.kind else {
2639            let kind = line_object.kind.human_friendly_kind_with_article();
2640            return Err(KclError::refactor(format!(
2641                "This constraint only works on Segments, but you selected {kind}"
2642            )));
2643        };
2644
2645        // Modify the line AST.
2646        self.mutate_ast(
2647            new_ast,
2648            line_id,
2649            AstMutateCommand::EditLine {
2650                start: new_start_ast,
2651                end: new_end_ast,
2652                construction: ctor.construction,
2653            },
2654        )?;
2655        Ok(())
2656    }
2657
2658    fn edit_arc(
2659        &mut self,
2660        new_ast: &mut ast::Node<ast::Program>,
2661        sketch: ObjectId,
2662        arc: ObjectId,
2663        ctor: ArcCtor,
2664    ) -> Result<(), KclError> {
2665        // Create updated KCL source from args.
2666        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2667        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2668        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2669
2670        // Look up existing sketch.
2671        let sketch_id = sketch;
2672        let sketch_object = self
2673            .scene_graph
2674            .objects
2675            .get(sketch_id.0)
2676            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2677        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2678            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2679        };
2680        sketch
2681            .segments
2682            .iter()
2683            .find(|o| **o == arc)
2684            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2685        // Look up existing arc.
2686        let arc_id = arc;
2687        let arc_object = self
2688            .scene_graph
2689            .objects
2690            .get(arc_id.0)
2691            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2692        let ObjectKind::Segment { .. } = &arc_object.kind else {
2693            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2694        };
2695
2696        // Modify the arc AST.
2697        self.mutate_ast(
2698            new_ast,
2699            arc_id,
2700            AstMutateCommand::EditArc {
2701                start: new_start_ast,
2702                end: new_end_ast,
2703                center: new_center_ast,
2704                construction: ctor.construction,
2705            },
2706        )?;
2707        Ok(())
2708    }
2709
2710    fn edit_circle(
2711        &mut self,
2712        new_ast: &mut ast::Node<ast::Program>,
2713        sketch: ObjectId,
2714        circle: ObjectId,
2715        ctor: CircleCtor,
2716    ) -> Result<(), KclError> {
2717        // Create updated KCL source from args.
2718        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2719        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2720
2721        // Look up existing sketch.
2722        let sketch_id = sketch;
2723        let sketch_object = self
2724            .scene_graph
2725            .objects
2726            .get(sketch_id.0)
2727            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2728        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2729            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2730        };
2731        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2732            KclError::refactor(format!(
2733                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2734            ))
2735        })?;
2736        // Look up existing circle.
2737        let circle_id = circle;
2738        let circle_object = self
2739            .scene_graph
2740            .objects
2741            .get(circle_id.0)
2742            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2743        let ObjectKind::Segment { .. } = &circle_object.kind else {
2744            return Err(KclError::refactor(format!(
2745                "Object is not a segment: {circle_object:?}"
2746            )));
2747        };
2748
2749        // Modify the circle AST.
2750        self.mutate_ast(
2751            new_ast,
2752            circle_id,
2753            AstMutateCommand::EditCircle {
2754                start: new_start_ast,
2755                center: new_center_ast,
2756                construction: ctor.construction,
2757            },
2758        )?;
2759        Ok(())
2760    }
2761
2762    fn delete_segment(
2763        &mut self,
2764        new_ast: &mut ast::Node<ast::Program>,
2765        sketch: ObjectId,
2766        segment_id: ObjectId,
2767    ) -> Result<(), KclError> {
2768        // Look up existing sketch.
2769        let sketch_id = sketch;
2770        let sketch_object = self
2771            .scene_graph
2772            .objects
2773            .get(sketch_id.0)
2774            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2775        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2776            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2777        };
2778        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2779            KclError::refactor(format!(
2780                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2781            ))
2782        })?;
2783        // Look up existing segment.
2784        let segment_object =
2785            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2786                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2787            })?;
2788        let ObjectKind::Segment { .. } = &segment_object.kind else {
2789            return Err(KclError::refactor(format!(
2790                "Object is not a segment, it is {}",
2791                segment_object.kind.human_friendly_kind_with_article()
2792            )));
2793        };
2794
2795        // Modify the AST to remove the segment.
2796        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2797        Ok(())
2798    }
2799
2800    fn delete_constraint(
2801        &mut self,
2802        new_ast: &mut ast::Node<ast::Program>,
2803        sketch: ObjectId,
2804        constraint_id: ObjectId,
2805    ) -> Result<(), KclError> {
2806        // Look up existing sketch.
2807        let sketch_id = sketch;
2808        let sketch_object = self
2809            .scene_graph
2810            .objects
2811            .get(sketch_id.0)
2812            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2813        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2814            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2815        };
2816        sketch
2817            .constraints
2818            .iter()
2819            .find(|o| **o == constraint_id)
2820            .ok_or_else(|| {
2821                KclError::refactor(format!(
2822                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2823                ))
2824            })?;
2825        // Look up existing constraint.
2826        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2827            KclError::refactor(format!(
2828                "Constraint not found in scene graph: constraint={constraint_id:?}"
2829            ))
2830        })?;
2831        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2832            return Err(KclError::refactor(format!(
2833                "Object is not a constraint, it is {}",
2834                constraint_object.kind.human_friendly_kind_with_article()
2835            )));
2836        };
2837
2838        // Modify the AST to remove the constraint.
2839        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2840        Ok(())
2841    }
2842
2843    fn edit_coincident_constraint(
2844        &mut self,
2845        new_ast: &mut ast::Node<ast::Program>,
2846        constraint_id: ObjectId,
2847        segments: Vec<ConstraintSegment>,
2848    ) -> Result<(), KclError> {
2849        if segments.len() < 2 {
2850            return Err(KclError::refactor(format!(
2851                "Coincident constraint must have at least 2 inputs, got {}",
2852                segments.len()
2853            )));
2854        }
2855
2856        let segment_asts = segments
2857            .iter()
2858            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2859            .collect::<Result<Vec<_>, _>>()?;
2860
2861        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2862            elements: segment_asts,
2863            digest: None,
2864            non_code_meta: Default::default(),
2865        })));
2866
2867        self.mutate_ast(
2868            new_ast,
2869            constraint_id,
2870            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2871        )?;
2872        Ok(())
2873    }
2874
2875    fn edit_horizontal_points_constraint(
2876        &mut self,
2877        new_ast: &mut ast::Node<ast::Program>,
2878        constraint_id: ObjectId,
2879        points: Vec<ConstraintSegment>,
2880    ) -> Result<(), KclError> {
2881        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2882    }
2883
2884    fn edit_vertical_points_constraint(
2885        &mut self,
2886        new_ast: &mut ast::Node<ast::Program>,
2887        constraint_id: ObjectId,
2888        points: Vec<ConstraintSegment>,
2889    ) -> Result<(), KclError> {
2890        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2891    }
2892
2893    fn edit_axis_points_constraint(
2894        &mut self,
2895        new_ast: &mut ast::Node<ast::Program>,
2896        constraint_id: ObjectId,
2897        points: Vec<ConstraintSegment>,
2898        constraint_name: &str,
2899    ) -> Result<(), KclError> {
2900        if points.len() < 2 {
2901            return Err(KclError::refactor(format!(
2902                "{constraint_name} points constraint must have at least 2 points, got {}",
2903                points.len()
2904            )));
2905        }
2906
2907        let point_asts = points
2908            .iter()
2909            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2910            .collect::<Result<Vec<_>, _>>()?;
2911
2912        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2913            elements: point_asts,
2914            digest: None,
2915            non_code_meta: Default::default(),
2916        })));
2917
2918        self.mutate_ast(
2919            new_ast,
2920            constraint_id,
2921            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2922        )?;
2923        Ok(())
2924    }
2925
2926    /// updates the equalLength constraint with the given lines
2927    fn edit_equal_length_constraint(
2928        &mut self,
2929        new_ast: &mut ast::Node<ast::Program>,
2930        constraint_id: ObjectId,
2931        lines: Vec<ObjectId>,
2932    ) -> Result<(), KclError> {
2933        if lines.len() < 2 {
2934            return Err(KclError::refactor(format!(
2935                "Lines equal length constraint must have at least 2 lines, got {}",
2936                lines.len()
2937            )));
2938        }
2939
2940        let line_asts = lines
2941            .iter()
2942            .map(|line_id| {
2943                let line_object = self
2944                    .scene_graph
2945                    .objects
2946                    .get(line_id.0)
2947                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2948                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2949                    let kind = line_object.kind.human_friendly_kind_with_article();
2950                    return Err(KclError::refactor(format!(
2951                        "This constraint only works on Segments, but you selected {kind}"
2952                    )));
2953                };
2954                let Segment::Line(_) = line_segment else {
2955                    let kind = line_segment.human_friendly_kind_with_article();
2956                    return Err(KclError::refactor(format!(
2957                        "Only lines can be made equal length, but you selected {kind}"
2958                    )));
2959                };
2960
2961                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2962            })
2963            .collect::<Result<Vec<_>, _>>()?;
2964
2965        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2966            elements: line_asts,
2967            digest: None,
2968            non_code_meta: Default::default(),
2969        })));
2970
2971        self.mutate_ast(
2972            new_ast,
2973            constraint_id,
2974            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2975        )?;
2976        Ok(())
2977    }
2978
2979    /// Updates the parallel constraint with the given lines.
2980    fn edit_parallel_constraint(
2981        &mut self,
2982        new_ast: &mut ast::Node<ast::Program>,
2983        constraint_id: ObjectId,
2984        lines: Vec<ObjectId>,
2985    ) -> Result<(), KclError> {
2986        if lines.len() < 2 {
2987            return Err(KclError::refactor(format!(
2988                "Parallel constraint must have at least 2 lines, got {}",
2989                lines.len()
2990            )));
2991        }
2992
2993        let line_asts = lines
2994            .iter()
2995            .map(|line_id| {
2996                let line_object = self
2997                    .scene_graph
2998                    .objects
2999                    .get(line_id.0)
3000                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3001                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3002                    let kind = line_object.kind.human_friendly_kind_with_article();
3003                    return Err(KclError::refactor(format!(
3004                        "This constraint only works on Segments, but you selected {kind}"
3005                    )));
3006                };
3007                let Segment::Line(_) = line_segment else {
3008                    let kind = line_segment.human_friendly_kind_with_article();
3009                    return Err(KclError::refactor(format!(
3010                        "Only lines can be made parallel, but you selected {kind}"
3011                    )));
3012                };
3013
3014                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3015            })
3016            .collect::<Result<Vec<_>, _>>()?;
3017
3018        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3019            elements: line_asts,
3020            digest: None,
3021            non_code_meta: Default::default(),
3022        })));
3023
3024        self.mutate_ast(
3025            new_ast,
3026            constraint_id,
3027            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3028        )?;
3029        Ok(())
3030    }
3031
3032    /// Updates the equalRadius constraint with the given segments.
3033    fn edit_equal_radius_constraint(
3034        &mut self,
3035        new_ast: &mut ast::Node<ast::Program>,
3036        constraint_id: ObjectId,
3037        input: Vec<ObjectId>,
3038    ) -> Result<(), KclError> {
3039        if input.len() < 2 {
3040            return Err(KclError::refactor(format!(
3041                "equalRadius constraint must have at least 2 segments, got {}",
3042                input.len()
3043            )));
3044        }
3045
3046        let input_asts = input
3047            .iter()
3048            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3049            .collect::<Result<Vec<_>, _>>()?;
3050
3051        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3052            elements: input_asts,
3053            digest: None,
3054            non_code_meta: Default::default(),
3055        })));
3056
3057        self.mutate_ast(
3058            new_ast,
3059            constraint_id,
3060            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3061        )?;
3062        Ok(())
3063    }
3064
3065    async fn execute_after_edit(
3066        &mut self,
3067        ctx: &ExecutorContext,
3068        sketch: ObjectId,
3069        sketch_block_ref: AstNodeRef,
3070        options: ExecuteAfterEditOptions,
3071        new_ast: &mut ast::Node<ast::Program>,
3072    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3073        // Convert to string source to create real source ranges.
3074        let new_source = source_from_ast(new_ast);
3075        // Parse the new KCL source.
3076        let (new_program, errors) = Program::parse(&new_source)
3077            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3078        if !errors.is_empty() {
3079            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3080                "Error parsing KCL source after editing: {errors:?}"
3081            ))));
3082        }
3083        let Some(new_program) = new_program else {
3084            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3085                "No AST produced after editing".to_string(),
3086            )));
3087        };
3088
3089        // TODO: sketch-api: make sure to only set this if there are no errors.
3090        self.program = new_program.clone();
3091
3092        // Truncate after the sketch block for mock execution.
3093        let is_delete = options.edit_kind.is_delete();
3094        let truncated_program = {
3095            let mut truncated_program = new_program;
3096            only_sketch_block(
3097                &mut truncated_program.ast,
3098                &sketch_block_ref,
3099                options.edit_kind.to_change_kind(),
3100            )
3101            .map_err(KclErrorWithOutputs::no_outputs)?;
3102            truncated_program
3103        };
3104
3105        // Execute.
3106        let use_warm_starts = !is_delete
3107            && matches!(
3108                options.sketch_var_update_mode,
3109                SketchVarUpdateMode::WarmStartOnly | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart
3110            );
3111        let mock_config =
3112            self.sketch_mock_config(sketch, is_delete, options.segment_ids_edited.clone(), use_warm_starts);
3113        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3114
3115        // Uses freedom_analysis: is_delete
3116        let outcome = self.update_state_after_exec(outcome, is_delete);
3117        match options.sketch_var_update_mode {
3118            SketchVarUpdateMode::CommitSolvedVars => {
3119                self.merge_committed_edit_sketch_var_warm_starts(sketch, &outcome, &options.segment_ids_edited);
3120            }
3121            SketchVarUpdateMode::WarmStartOnly | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart => {
3122                self.replace_sketch_var_warm_starts(sketch, &outcome);
3123            }
3124        }
3125
3126        let new_source = match options.sketch_var_update_mode {
3127            SketchVarUpdateMode::WarmStartOnly => new_source,
3128            SketchVarUpdateMode::CommitSolvedVars | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart => {
3129                self.commit_var_solutions_to_program(&outcome)?
3130            }
3131        };
3132
3133        let src_delta = SourceDelta { text: new_source };
3134        let scene_graph_delta = SceneGraphDelta {
3135            new_graph: self.scene_graph.clone(),
3136            invalidates_ids: is_delete,
3137            new_objects: Vec::new(),
3138            exec_outcome: outcome,
3139        };
3140        Ok((src_delta, scene_graph_delta))
3141    }
3142
3143    async fn execute_after_delete_sketch(
3144        &mut self,
3145        ctx: &ExecutorContext,
3146        new_ast: &mut ast::Node<ast::Program>,
3147    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3148        // Convert to string source to create real source ranges.
3149        let new_source = source_from_ast(new_ast);
3150        // Parse the new KCL source.
3151        let (new_program, errors) = Program::parse(&new_source)
3152            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3153        if !errors.is_empty() {
3154            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3155                "Error parsing KCL source after editing: {errors:?}"
3156            ))));
3157        }
3158        let Some(new_program) = new_program else {
3159            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3160                "No AST produced after editing".to_string(),
3161            )));
3162        };
3163
3164        // Make sure to only set this if there are no errors.
3165        self.program = new_program.clone();
3166
3167        // We deleted the entire sketch block. It doesn't make sense to truncate
3168        // and execute only the sketch block. We execute the whole program with
3169        // a real engine.
3170
3171        // Execute.
3172        let outcome = ctx.run_with_caching(new_program).await?;
3173        let freedom_analysis_ran = true;
3174
3175        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3176
3177        let src_delta = SourceDelta { text: new_source };
3178        let scene_graph_delta = SceneGraphDelta {
3179            new_graph: self.scene_graph.clone(),
3180            invalidates_ids: true,
3181            new_objects: Vec::new(),
3182            exec_outcome: outcome,
3183        };
3184        Ok((src_delta, scene_graph_delta))
3185    }
3186
3187    /// Map a point object id into an AST reference expression for use in
3188    /// constraints. If the point is owned by a segment (line or arc), we
3189    /// reference the appropriate property on that segment (e.g. `line1.start`,
3190    /// `arc1.center`). Otherwise we reference the point directly.
3191    fn point_id_to_ast_reference(
3192        &self,
3193        point_id: ObjectId,
3194        new_ast: &mut ast::Node<ast::Program>,
3195    ) -> Result<ast::Expr, KclError> {
3196        let point_object = self
3197            .scene_graph
3198            .objects
3199            .get(point_id.0)
3200            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3201        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3202            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3203        };
3204        let Segment::Point(point) = point_segment else {
3205            return Err(KclError::refactor(format!(
3206                "Only points are currently supported: {point_object:?}"
3207            )));
3208        };
3209
3210        if let Some(owner_id) = point.owner {
3211            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3212                KclError::refactor(format!(
3213                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3214                ))
3215            })?;
3216            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3217                return Err(KclError::refactor(format!(
3218                    "Owner of point is not a segment, but found {}",
3219                    owner_object.kind.human_friendly_kind_with_article()
3220                )));
3221            };
3222
3223            match owner_segment {
3224                Segment::Line(line) => {
3225                    let property = if line.start == point_id {
3226                        LINE_PROPERTY_START
3227                    } else if line.end == point_id {
3228                        LINE_PROPERTY_END
3229                    } else {
3230                        return Err(KclError::refactor(format!(
3231                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3232                        )));
3233                    };
3234                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3235                }
3236                Segment::Arc(arc) => {
3237                    let property = if arc.start == point_id {
3238                        ARC_PROPERTY_START
3239                    } else if arc.end == point_id {
3240                        ARC_PROPERTY_END
3241                    } else if arc.center == point_id {
3242                        ARC_PROPERTY_CENTER
3243                    } else {
3244                        return Err(KclError::refactor(format!(
3245                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3246                        )));
3247                    };
3248                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3249                }
3250                Segment::Circle(circle) => {
3251                    let property = if circle.start == point_id {
3252                        CIRCLE_PROPERTY_START
3253                    } else if circle.center == point_id {
3254                        CIRCLE_PROPERTY_CENTER
3255                    } else {
3256                        return Err(KclError::refactor(format!(
3257                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3258                        )));
3259                    };
3260                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3261                }
3262                _ => Err(KclError::refactor(format!(
3263                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3264                ))),
3265            }
3266        } else {
3267            // Standalone point.
3268            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3269        }
3270    }
3271
3272    fn coincident_segment_to_ast(
3273        &self,
3274        segment: &ConstraintSegment,
3275        new_ast: &mut ast::Node<ast::Program>,
3276    ) -> Result<ast::Expr, KclError> {
3277        match segment {
3278            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3279            ConstraintSegment::Segment(segment_id) => {
3280                let segment_object = self
3281                    .scene_graph
3282                    .objects
3283                    .get(segment_id.0)
3284                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3285                let ObjectKind::Segment { segment } = &segment_object.kind else {
3286                    return Err(KclError::refactor(format!(
3287                        "Object is not a segment, it is {}",
3288                        segment_object.kind.human_friendly_kind_with_article()
3289                    )));
3290                };
3291
3292                match segment {
3293                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3294                    Segment::Line(_) => {
3295                        get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3296                    }
3297                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3298                    Segment::Circle(_) => {
3299                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3300                    }
3301                }
3302            }
3303        }
3304    }
3305
3306    fn axis_constraint_segment_to_ast(
3307        &self,
3308        segment: &ConstraintSegment,
3309        new_ast: &mut ast::Node<ast::Program>,
3310    ) -> Result<ast::Expr, KclError> {
3311        match segment {
3312            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3313            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3314        }
3315    }
3316
3317    async fn add_coincident(
3318        &mut self,
3319        sketch: ObjectId,
3320        coincident: Coincident,
3321        new_ast: &mut ast::Node<ast::Program>,
3322    ) -> Result<AstNodeRef, KclError> {
3323        let sketch_id = sketch;
3324        let segment_asts = coincident
3325            .segments
3326            .iter()
3327            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3328            .collect::<Result<Vec<_>, _>>()?;
3329        if segment_asts.len() < 2 {
3330            return Err(KclError::refactor(format!(
3331                "Coincident constraint must have at least 2 inputs, got {}",
3332                segment_asts.len()
3333            )));
3334        }
3335
3336        // Create the coincident() call using shared helper.
3337        let coincident_ast = create_coincident_ast(segment_asts);
3338
3339        // Add the line to the AST of the sketch block.
3340        let (sketch_block_ref, _) = self.mutate_ast(
3341            new_ast,
3342            sketch_id,
3343            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3344        )?;
3345        Ok(sketch_block_ref)
3346    }
3347
3348    async fn add_distance(
3349        &mut self,
3350        sketch: ObjectId,
3351        distance: Distance,
3352        new_ast: &mut ast::Node<ast::Program>,
3353    ) -> Result<AstNodeRef, KclError> {
3354        let sketch_id = sketch;
3355        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3356            [pt0, pt1] => [
3357                self.coincident_segment_to_ast(pt0, new_ast)?,
3358                self.coincident_segment_to_ast(pt1, new_ast)?,
3359            ],
3360            _ => {
3361                return Err(KclError::refactor(format!(
3362                    "Distance constraint must have exactly 2 points, got {}",
3363                    distance.points.len()
3364                )));
3365            }
3366        };
3367
3368        let arguments = match &distance.label_position {
3369            Some(label_position) => vec![ast::LabeledArg {
3370                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3371                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3372            }],
3373            None => Default::default(),
3374        };
3375
3376        // Create the distance() call.
3377        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3378            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3379            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3380                ast::ArrayExpression {
3381                    elements: vec![pt0_ast, pt1_ast],
3382                    digest: None,
3383                    non_code_meta: Default::default(),
3384                },
3385            )))),
3386            arguments,
3387            digest: None,
3388            non_code_meta: Default::default(),
3389        })));
3390        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3391            left: distance_call_ast,
3392            operator: ast::BinaryOperator::Eq,
3393            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3394                value: ast::LiteralValue::Number {
3395                    value: distance.distance.value,
3396                    suffix: distance.distance.units,
3397                },
3398                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3399                    KclError::refactor(format!(
3400                        "Could not format numeric suffix: {:?}",
3401                        distance.distance.units
3402                    ))
3403                })?,
3404                digest: None,
3405            }))),
3406            digest: None,
3407        })));
3408
3409        // Add the line to the AST of the sketch block.
3410        let (sketch_block_ref, _) = self.mutate_ast(
3411            new_ast,
3412            sketch_id,
3413            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3414        )?;
3415        Ok(sketch_block_ref)
3416    }
3417
3418    async fn add_angle(
3419        &mut self,
3420        sketch: ObjectId,
3421        angle: Angle,
3422        new_ast: &mut ast::Node<ast::Program>,
3423    ) -> Result<AstNodeRef, KclError> {
3424        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3425            return Err(KclError::refactor(format!(
3426                "Angle constraint must have exactly 2 lines, got {}",
3427                angle.lines.len()
3428            )));
3429        };
3430        let sketch_id = sketch;
3431
3432        // Map the runtime objects back to variable names.
3433        let line0_object = self
3434            .scene_graph
3435            .objects
3436            .get(l0_id.0)
3437            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3438        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3439            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3440        };
3441        let Segment::Line(_) = line0_segment else {
3442            return Err(KclError::refactor(format!(
3443                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3444            )));
3445        };
3446        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3447
3448        let line1_object = self
3449            .scene_graph
3450            .objects
3451            .get(l1_id.0)
3452            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3453        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3454            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3455        };
3456        let Segment::Line(_) = line1_segment else {
3457            return Err(KclError::refactor(format!(
3458                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3459            )));
3460        };
3461        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3462
3463        // Create the angle() call.
3464        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3465            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3466            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3467                ast::ArrayExpression {
3468                    elements: vec![l0_ast, l1_ast],
3469                    digest: None,
3470                    non_code_meta: Default::default(),
3471                },
3472            )))),
3473            arguments: Default::default(),
3474            digest: None,
3475            non_code_meta: Default::default(),
3476        })));
3477        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3478            left: angle_call_ast,
3479            operator: ast::BinaryOperator::Eq,
3480            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3481                value: ast::LiteralValue::Number {
3482                    value: angle.angle.value,
3483                    suffix: angle.angle.units,
3484                },
3485                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3486                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3487                })?,
3488                digest: None,
3489            }))),
3490            digest: None,
3491        })));
3492
3493        // Add the line to the AST of the sketch block.
3494        let (sketch_block_ref, _) = self.mutate_ast(
3495            new_ast,
3496            sketch_id,
3497            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3498        )?;
3499        Ok(sketch_block_ref)
3500    }
3501
3502    async fn add_tangent(
3503        &mut self,
3504        sketch: ObjectId,
3505        tangent: Tangent,
3506        new_ast: &mut ast::Node<ast::Program>,
3507    ) -> Result<AstNodeRef, KclError> {
3508        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3509            return Err(KclError::refactor(format!(
3510                "Tangent constraint must have exactly 2 segments, got {}",
3511                tangent.input.len()
3512            )));
3513        };
3514        let sketch_id = sketch;
3515
3516        let seg0_object = self
3517            .scene_graph
3518            .objects
3519            .get(seg0_id.0)
3520            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3521        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3522            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3523        };
3524        let seg0_ast = match seg0_segment {
3525            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3526            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3527            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3528            _ => {
3529                return Err(KclError::refactor(format!(
3530                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3531                )));
3532            }
3533        };
3534
3535        let seg1_object = self
3536            .scene_graph
3537            .objects
3538            .get(seg1_id.0)
3539            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3540        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3541            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3542        };
3543        let seg1_ast = match seg1_segment {
3544            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3545            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3546            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3547            _ => {
3548                return Err(KclError::refactor(format!(
3549                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3550                )));
3551            }
3552        };
3553
3554        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3555        let (sketch_block_ref, _) = self.mutate_ast(
3556            new_ast,
3557            sketch_id,
3558            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3559        )?;
3560        Ok(sketch_block_ref)
3561    }
3562
3563    async fn add_symmetric(
3564        &mut self,
3565        sketch: ObjectId,
3566        symmetric: Symmetric,
3567        new_ast: &mut ast::Node<ast::Program>,
3568    ) -> Result<AstNodeRef, KclError> {
3569        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3570            return Err(KclError::refactor(format!(
3571                "Symmetric constraint must have exactly 2 inputs, got {}",
3572                symmetric.input.len()
3573            )));
3574        };
3575        let sketch_id = sketch;
3576
3577        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3578        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3579        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3580
3581        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3582        let (sketch_block_ref, _) = self.mutate_ast(
3583            new_ast,
3584            sketch_id,
3585            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3586        )?;
3587        Ok(sketch_block_ref)
3588    }
3589
3590    async fn add_midpoint(
3591        &mut self,
3592        sketch: ObjectId,
3593        midpoint: Midpoint,
3594        new_ast: &mut ast::Node<ast::Program>,
3595    ) -> Result<AstNodeRef, KclError> {
3596        let sketch_id = sketch;
3597        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3598
3599        let segment_object = self
3600            .scene_graph
3601            .objects
3602            .get(midpoint.segment.0)
3603            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3604        let ObjectKind::Segment {
3605            segment: midpoint_segment,
3606        } = &segment_object.kind
3607        else {
3608            return Err(KclError::refactor(format!(
3609                "Object must be a segment, but it was {}",
3610                segment_object.kind.human_friendly_kind_with_article()
3611            )));
3612        };
3613        let segment_ast = match midpoint_segment {
3614            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3615            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3616            _ => {
3617                return Err(KclError::refactor(format!(
3618                    "Midpoint target must be a line or arc segment but it was {}",
3619                    midpoint_segment.human_friendly_kind_with_article()
3620                )));
3621            }
3622        };
3623
3624        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3625        let (sketch_block_ref, _) = self.mutate_ast(
3626            new_ast,
3627            sketch_id,
3628            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3629        )?;
3630        Ok(sketch_block_ref)
3631    }
3632
3633    async fn add_equal_radius(
3634        &mut self,
3635        sketch: ObjectId,
3636        equal_radius: EqualRadius,
3637        new_ast: &mut ast::Node<ast::Program>,
3638    ) -> Result<AstNodeRef, KclError> {
3639        if equal_radius.input.len() < 2 {
3640            return Err(KclError::refactor(format!(
3641                "equalRadius constraint must have at least 2 segments, got {}",
3642                equal_radius.input.len()
3643            )));
3644        }
3645
3646        let sketch_id = sketch;
3647        let input_asts = equal_radius
3648            .input
3649            .iter()
3650            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3651            .collect::<Result<Vec<_>, _>>()?;
3652
3653        let equal_radius_ast = create_equal_radius_ast(input_asts);
3654        let (sketch_block_ref, _) = self.mutate_ast(
3655            new_ast,
3656            sketch_id,
3657            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3658        )?;
3659        Ok(sketch_block_ref)
3660    }
3661
3662    async fn add_radius(
3663        &mut self,
3664        sketch: ObjectId,
3665        radius: Radius,
3666        new_ast: &mut ast::Node<ast::Program>,
3667    ) -> Result<AstNodeRef, KclError> {
3668        let params = ArcSizeConstraintParams {
3669            points: vec![radius.arc],
3670            function_name: RADIUS_FN,
3671            value: radius.radius.value,
3672            units: radius.radius.units,
3673            label_position: radius.label_position,
3674            constraint_type_name: "Radius",
3675        };
3676        self.add_arc_size_constraint(sketch, params, new_ast).await
3677    }
3678
3679    async fn add_diameter(
3680        &mut self,
3681        sketch: ObjectId,
3682        diameter: Diameter,
3683        new_ast: &mut ast::Node<ast::Program>,
3684    ) -> Result<AstNodeRef, KclError> {
3685        let params = ArcSizeConstraintParams {
3686            points: vec![diameter.arc],
3687            function_name: DIAMETER_FN,
3688            value: diameter.diameter.value,
3689            units: diameter.diameter.units,
3690            label_position: diameter.label_position,
3691            constraint_type_name: "Diameter",
3692        };
3693        self.add_arc_size_constraint(sketch, params, new_ast).await
3694    }
3695
3696    async fn add_fixed_constraints(
3697        &mut self,
3698        sketch: ObjectId,
3699        points: Vec<FixedPoint>,
3700        new_ast: &mut ast::Node<ast::Program>,
3701    ) -> Result<AstNodeRef, KclError> {
3702        let mut sketch_block_ref = None;
3703
3704        for fixed_point in points {
3705            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3706            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3707                .map_err(|err| KclError::refactor(err.to_string()))?;
3708
3709            let (sketch_ref, _) = self.mutate_ast(
3710                new_ast,
3711                sketch,
3712                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3713            )?;
3714            sketch_block_ref = Some(sketch_ref);
3715        }
3716
3717        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3718    }
3719
3720    async fn add_arc_size_constraint(
3721        &mut self,
3722        sketch: ObjectId,
3723        params: ArcSizeConstraintParams,
3724        new_ast: &mut ast::Node<ast::Program>,
3725    ) -> Result<AstNodeRef, KclError> {
3726        let sketch_id = sketch;
3727
3728        // Constraint must have exactly 1 argument (arc segment)
3729        if params.points.len() != 1 {
3730            return Err(KclError::refactor(format!(
3731                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3732                params.constraint_type_name,
3733                params.points.len()
3734            )));
3735        }
3736
3737        let arc_id = params.points[0];
3738        let arc_object = self
3739            .scene_graph
3740            .objects
3741            .get(arc_id.0)
3742            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3743        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3744            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3745        };
3746        let ref_type = match arc_segment {
3747            Segment::Arc(_) => ARC_VARIABLE,
3748            Segment::Circle(_) => CIRCLE_VARIABLE,
3749            _ => {
3750                return Err(KclError::refactor(format!(
3751                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3752                    params.constraint_type_name
3753                )));
3754            }
3755        };
3756        // Reference the arc/circle segment directly
3757        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3758        let arguments = match &params.label_position {
3759            Some(label_position) => vec![ast::LabeledArg {
3760                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3761                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3762            }],
3763            None => Default::default(),
3764        };
3765
3766        // Create the function call.
3767        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3768            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3769            unlabeled: Some(arc_ast),
3770            arguments,
3771            digest: None,
3772            non_code_meta: Default::default(),
3773        })));
3774        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3775            left: call_ast,
3776            operator: ast::BinaryOperator::Eq,
3777            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3778                value: ast::LiteralValue::Number {
3779                    value: params.value,
3780                    suffix: params.units,
3781                },
3782                raw: format_number_literal(params.value, params.units, None)
3783                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3784                digest: None,
3785            }))),
3786            digest: None,
3787        })));
3788
3789        // Add the line to the AST of the sketch block.
3790        let (sketch_block_ref, _) = self.mutate_ast(
3791            new_ast,
3792            sketch_id,
3793            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3794        )?;
3795        Ok(sketch_block_ref)
3796    }
3797
3798    async fn add_horizontal_distance(
3799        &mut self,
3800        sketch: ObjectId,
3801        distance: Distance,
3802        new_ast: &mut ast::Node<ast::Program>,
3803    ) -> Result<AstNodeRef, KclError> {
3804        let sketch_id = sketch;
3805        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3806            [pt0, pt1] => [
3807                self.coincident_segment_to_ast(pt0, new_ast)?,
3808                self.coincident_segment_to_ast(pt1, new_ast)?,
3809            ],
3810            _ => {
3811                return Err(KclError::refactor(format!(
3812                    "Horizontal distance constraint must have exactly 2 points, got {}",
3813                    distance.points.len()
3814                )));
3815            }
3816        };
3817
3818        let arguments = match &distance.label_position {
3819            Some(label_position) => vec![ast::LabeledArg {
3820                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3821                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3822            }],
3823            None => Default::default(),
3824        };
3825
3826        // Create the horizontalDistance() call.
3827        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3828            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3829            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3830                ast::ArrayExpression {
3831                    elements: vec![pt0_ast, pt1_ast],
3832                    digest: None,
3833                    non_code_meta: Default::default(),
3834                },
3835            )))),
3836            arguments,
3837            digest: None,
3838            non_code_meta: Default::default(),
3839        })));
3840        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3841            left: distance_call_ast,
3842            operator: ast::BinaryOperator::Eq,
3843            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3844                value: ast::LiteralValue::Number {
3845                    value: distance.distance.value,
3846                    suffix: distance.distance.units,
3847                },
3848                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3849                    KclError::refactor(format!(
3850                        "Could not format numeric suffix: {:?}",
3851                        distance.distance.units
3852                    ))
3853                })?,
3854                digest: None,
3855            }))),
3856            digest: None,
3857        })));
3858
3859        // Add the line to the AST of the sketch block.
3860        let (sketch_block_ref, _) = self.mutate_ast(
3861            new_ast,
3862            sketch_id,
3863            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3864        )?;
3865        Ok(sketch_block_ref)
3866    }
3867
3868    async fn add_vertical_distance(
3869        &mut self,
3870        sketch: ObjectId,
3871        distance: Distance,
3872        new_ast: &mut ast::Node<ast::Program>,
3873    ) -> Result<AstNodeRef, KclError> {
3874        let sketch_id = sketch;
3875        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3876            [pt0, pt1] => [
3877                self.coincident_segment_to_ast(pt0, new_ast)?,
3878                self.coincident_segment_to_ast(pt1, new_ast)?,
3879            ],
3880            _ => {
3881                return Err(KclError::refactor(format!(
3882                    "Vertical distance constraint must have exactly 2 points, got {}",
3883                    distance.points.len()
3884                )));
3885            }
3886        };
3887
3888        let arguments = match &distance.label_position {
3889            Some(label_position) => vec![ast::LabeledArg {
3890                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3891                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3892            }],
3893            None => Default::default(),
3894        };
3895
3896        // Create the verticalDistance() call.
3897        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3898            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3899            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3900                ast::ArrayExpression {
3901                    elements: vec![pt0_ast, pt1_ast],
3902                    digest: None,
3903                    non_code_meta: Default::default(),
3904                },
3905            )))),
3906            arguments,
3907            digest: None,
3908            non_code_meta: Default::default(),
3909        })));
3910        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3911            left: distance_call_ast,
3912            operator: ast::BinaryOperator::Eq,
3913            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3914                value: ast::LiteralValue::Number {
3915                    value: distance.distance.value,
3916                    suffix: distance.distance.units,
3917                },
3918                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3919                    KclError::refactor(format!(
3920                        "Could not format numeric suffix: {:?}",
3921                        distance.distance.units
3922                    ))
3923                })?,
3924                digest: None,
3925            }))),
3926            digest: None,
3927        })));
3928
3929        // Add the line to the AST of the sketch block.
3930        let (sketch_block_ref, _) = self.mutate_ast(
3931            new_ast,
3932            sketch_id,
3933            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3934        )?;
3935        Ok(sketch_block_ref)
3936    }
3937
3938    async fn add_horizontal(
3939        &mut self,
3940        sketch: ObjectId,
3941        horizontal: Horizontal,
3942        new_ast: &mut ast::Node<ast::Program>,
3943    ) -> Result<AstNodeRef, KclError> {
3944        let sketch_id = sketch;
3945
3946        // Map the runtime objects back to variable names.
3947        let first_arg_ast = match horizontal {
3948            Horizontal::Line { line } => {
3949                let line_object = self
3950                    .scene_graph
3951                    .objects
3952                    .get(line.0)
3953                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3954                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3955                    let kind = line_object.kind.human_friendly_kind_with_article();
3956                    return Err(KclError::refactor(format!(
3957                        "This constraint only works on Segments, but you selected {kind}"
3958                    )));
3959                };
3960                let Segment::Line(_) = line_segment else {
3961                    return Err(KclError::refactor(format!(
3962                        "Only lines can be made horizontal, but you selected {}",
3963                        line_segment.human_friendly_kind_with_article(),
3964                    )));
3965                };
3966                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3967            }
3968            Horizontal::Points { points } => {
3969                let point_asts = points
3970                    .iter()
3971                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3972                    .collect::<Result<Vec<_>, _>>()?;
3973                ast::ArrayExpression::new(point_asts).into()
3974            }
3975        };
3976
3977        // Create the horizontal() call using shared helper.
3978        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3979
3980        // Add the line to the AST of the sketch block.
3981        let (sketch_block_ref, _) = self.mutate_ast(
3982            new_ast,
3983            sketch_id,
3984            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3985        )?;
3986        Ok(sketch_block_ref)
3987    }
3988
3989    async fn add_lines_equal_length(
3990        &mut self,
3991        sketch: ObjectId,
3992        lines_equal_length: LinesEqualLength,
3993        new_ast: &mut ast::Node<ast::Program>,
3994    ) -> Result<AstNodeRef, KclError> {
3995        if lines_equal_length.lines.len() < 2 {
3996            return Err(KclError::refactor(format!(
3997                "Lines equal length constraint must have at least 2 lines, got {}",
3998                lines_equal_length.lines.len()
3999            )));
4000        };
4001
4002        let sketch_id = sketch;
4003
4004        // Map the runtime objects back to variable names.
4005        let line_asts = lines_equal_length
4006            .lines
4007            .iter()
4008            .map(|line_id| {
4009                let line_object = self
4010                    .scene_graph
4011                    .objects
4012                    .get(line_id.0)
4013                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4014                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4015                    let kind = line_object.kind.human_friendly_kind_with_article();
4016                    return Err(KclError::refactor(format!(
4017                        "This constraint only works on Segments, but you selected {kind}"
4018                    )));
4019                };
4020                let Segment::Line(_) = line_segment else {
4021                    let kind = line_segment.human_friendly_kind_with_article();
4022                    return Err(KclError::refactor(format!(
4023                        "Only lines can be made equal length, but you selected {kind}"
4024                    )));
4025                };
4026
4027                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
4028            })
4029            .collect::<Result<Vec<_>, _>>()?;
4030
4031        // Create the equalLength() call using shared helper.
4032        let equal_length_ast = create_equal_length_ast(line_asts);
4033
4034        // Add the constraint to the AST of the sketch block.
4035        let (sketch_block_ref, _) = self.mutate_ast(
4036            new_ast,
4037            sketch_id,
4038            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4039        )?;
4040        Ok(sketch_block_ref)
4041    }
4042
4043    fn equal_radius_segment_id_to_ast_reference(
4044        &mut self,
4045        segment_id: ObjectId,
4046        new_ast: &mut ast::Node<ast::Program>,
4047    ) -> Result<ast::Expr, KclError> {
4048        let segment_object = self
4049            .scene_graph
4050            .objects
4051            .get(segment_id.0)
4052            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4053        let ObjectKind::Segment { segment } = &segment_object.kind else {
4054            return Err(KclError::refactor(format!(
4055                "Object is not a segment, it was {}",
4056                segment_object.kind.human_friendly_kind_with_article()
4057            )));
4058        };
4059
4060        let ref_type = match segment {
4061            Segment::Arc(_) => ARC_VARIABLE,
4062            Segment::Circle(_) => CIRCLE_VARIABLE,
4063            _ => {
4064                return Err(KclError::refactor(format!(
4065                    "equalRadius supports only arc/circle segments, got {}",
4066                    segment.human_friendly_kind_with_article()
4067                )));
4068            }
4069        };
4070
4071        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4072    }
4073
4074    fn symmetric_input_id_to_ast_reference(
4075        &mut self,
4076        segment_id: ObjectId,
4077        new_ast: &mut ast::Node<ast::Program>,
4078    ) -> Result<ast::Expr, KclError> {
4079        let segment_object = self
4080            .scene_graph
4081            .objects
4082            .get(segment_id.0)
4083            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4084        let ObjectKind::Segment { segment } = &segment_object.kind else {
4085            return Err(KclError::refactor(format!(
4086                "Object is not a segment, it was {}",
4087                segment_object.kind.human_friendly_kind_with_article()
4088            )));
4089        };
4090
4091        match segment {
4092            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4093            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4094            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4095            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4096        }
4097    }
4098
4099    fn symmetric_axis_id_to_ast_reference(
4100        &mut self,
4101        segment_id: ObjectId,
4102        new_ast: &mut ast::Node<ast::Program>,
4103    ) -> Result<ast::Expr, KclError> {
4104        let segment_object = self
4105            .scene_graph
4106            .objects
4107            .get(segment_id.0)
4108            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4109        let ObjectKind::Segment { segment } = &segment_object.kind else {
4110            return Err(KclError::refactor(format!(
4111                "Object is not a segment, it was {}",
4112                segment_object.kind.human_friendly_kind_with_article()
4113            )));
4114        };
4115        match segment {
4116            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4117            _ => Err(KclError::refactor(format!(
4118                "Symmetric axis must be a line, got {}",
4119                segment.human_friendly_kind_with_article()
4120            ))),
4121        }
4122    }
4123
4124    async fn add_parallel(
4125        &mut self,
4126        sketch: ObjectId,
4127        parallel: Parallel,
4128        new_ast: &mut ast::Node<ast::Program>,
4129    ) -> Result<AstNodeRef, KclError> {
4130        if parallel.lines.len() < 2 {
4131            return Err(KclError::refactor(format!(
4132                "Parallel constraint must have at least 2 lines, got {}",
4133                parallel.lines.len()
4134            )));
4135        };
4136
4137        let sketch_id = sketch;
4138
4139        let line_asts = parallel
4140            .lines
4141            .iter()
4142            .map(|line_id| {
4143                let line_object = self
4144                    .scene_graph
4145                    .objects
4146                    .get(line_id.0)
4147                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4148                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4149                    let kind = line_object.kind.human_friendly_kind_with_article();
4150                    return Err(KclError::refactor(format!(
4151                        "This constraint only works on Segments, but you selected {kind}"
4152                    )));
4153                };
4154                let Segment::Line(_) = line_segment else {
4155                    let kind = line_segment.human_friendly_kind_with_article();
4156                    return Err(KclError::refactor(format!(
4157                        "Only lines can be made parallel, but you selected {kind}"
4158                    )));
4159                };
4160
4161                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
4162            })
4163            .collect::<Result<Vec<_>, _>>()?;
4164
4165        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4166            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4167            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4168                ast::ArrayExpression {
4169                    elements: line_asts,
4170                    digest: None,
4171                    non_code_meta: Default::default(),
4172                },
4173            )))),
4174            arguments: Default::default(),
4175            digest: None,
4176            non_code_meta: Default::default(),
4177        })));
4178
4179        let (sketch_block_ref, _) = self.mutate_ast(
4180            new_ast,
4181            sketch_id,
4182            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4183        )?;
4184        Ok(sketch_block_ref)
4185    }
4186
4187    async fn add_perpendicular(
4188        &mut self,
4189        sketch: ObjectId,
4190        perpendicular: Perpendicular,
4191        new_ast: &mut ast::Node<ast::Program>,
4192    ) -> Result<AstNodeRef, KclError> {
4193        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4194            .await
4195    }
4196
4197    async fn add_lines_at_angle_constraint(
4198        &mut self,
4199        sketch: ObjectId,
4200        angle_kind: LinesAtAngleKind,
4201        lines: Vec<ObjectId>,
4202        new_ast: &mut ast::Node<ast::Program>,
4203    ) -> Result<AstNodeRef, KclError> {
4204        let &[line0_id, line1_id] = lines.as_slice() else {
4205            return Err(KclError::refactor(format!(
4206                "{} constraint must have exactly 2 lines, got {}",
4207                angle_kind.to_function_name(),
4208                lines.len()
4209            )));
4210        };
4211
4212        let sketch_id = sketch;
4213
4214        // Map the runtime objects back to variable names.
4215        let line0_object = self
4216            .scene_graph
4217            .objects
4218            .get(line0_id.0)
4219            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4220        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4221            let kind = line0_object.kind.human_friendly_kind_with_article();
4222            return Err(KclError::refactor(format!(
4223                "This constraint only works on Segments, but you selected {kind}"
4224            )));
4225        };
4226        let Segment::Line(_) = line0_segment else {
4227            return Err(KclError::refactor(format!(
4228                "Only lines can be made {}, but you selected {}",
4229                angle_kind.to_function_name(),
4230                line0_segment.human_friendly_kind_with_article(),
4231            )));
4232        };
4233        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
4234
4235        let line1_object = self
4236            .scene_graph
4237            .objects
4238            .get(line1_id.0)
4239            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4240        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4241            let kind = line1_object.kind.human_friendly_kind_with_article();
4242            return Err(KclError::refactor(format!(
4243                "This constraint only works on Segments, but you selected {kind}"
4244            )));
4245        };
4246        let Segment::Line(_) = line1_segment else {
4247            return Err(KclError::refactor(format!(
4248                "Only lines can be made {}, but you selected {}",
4249                angle_kind.to_function_name(),
4250                line1_segment.human_friendly_kind_with_article(),
4251            )));
4252        };
4253        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
4254
4255        // Create the parallel() or perpendicular() call.
4256        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4257            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4258            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4259                ast::ArrayExpression {
4260                    elements: vec![line0_ast, line1_ast],
4261                    digest: None,
4262                    non_code_meta: Default::default(),
4263                },
4264            )))),
4265            arguments: Default::default(),
4266            digest: None,
4267            non_code_meta: Default::default(),
4268        })));
4269
4270        // Add the constraint to the AST of the sketch block.
4271        let (sketch_block_ref, _) = self.mutate_ast(
4272            new_ast,
4273            sketch_id,
4274            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4275        )?;
4276        Ok(sketch_block_ref)
4277    }
4278
4279    async fn add_vertical(
4280        &mut self,
4281        sketch: ObjectId,
4282        vertical: Vertical,
4283        new_ast: &mut ast::Node<ast::Program>,
4284    ) -> Result<AstNodeRef, KclError> {
4285        let sketch_id = sketch;
4286
4287        let first_arg_ast = match vertical {
4288            Vertical::Line { line } => {
4289                // Map the runtime objects back to variable names.
4290                let line_object = self
4291                    .scene_graph
4292                    .objects
4293                    .get(line.0)
4294                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4295                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4296                    let kind = line_object.kind.human_friendly_kind_with_article();
4297                    return Err(KclError::refactor(format!(
4298                        "This constraint only works on Segments, but you selected {kind}"
4299                    )));
4300                };
4301                let Segment::Line(_) = line_segment else {
4302                    return Err(KclError::refactor(format!(
4303                        "Only lines can be made vertical, but you selected {}",
4304                        line_segment.human_friendly_kind_with_article()
4305                    )));
4306                };
4307                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4308            }
4309            Vertical::Points { points } => {
4310                let point_asts = points
4311                    .iter()
4312                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4313                    .collect::<Result<Vec<_>, _>>()?;
4314                ast::ArrayExpression::new(point_asts).into()
4315            }
4316        };
4317
4318        // Create the vertical() call using shared helper.
4319        let vertical_ast = create_vertical_ast(first_arg_ast);
4320
4321        // Add the line to the AST of the sketch block.
4322        let (sketch_block_ref, _) = self.mutate_ast(
4323            new_ast,
4324            sketch_id,
4325            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4326        )?;
4327        Ok(sketch_block_ref)
4328    }
4329
4330    async fn execute_after_add_constraint(
4331        &mut self,
4332        ctx: &ExecutorContext,
4333        sketch_id: ObjectId,
4334        sketch_block_ref: AstNodeRef,
4335        new_ast: &mut ast::Node<ast::Program>,
4336    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4337        // Convert to string source to create real source ranges.
4338        let new_source = source_from_ast(new_ast);
4339        // Parse the new KCL source.
4340        let (new_program, errors) = Program::parse(&new_source)
4341            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4342        if !errors.is_empty() {
4343            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4344                "Error parsing KCL source after adding constraint: {errors:?}"
4345            ))));
4346        }
4347        let Some(new_program) = new_program else {
4348            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4349                "No AST produced after adding constraint".to_string(),
4350            )));
4351        };
4352        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4353            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4354                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4355            )))
4356        })?;
4357
4358        // Truncate after the sketch block for mock execution.
4359        // Use a clone so we don't mutate new_program yet
4360        let mut truncated_program = new_program.clone();
4361        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4362            .map_err(KclErrorWithOutputs::no_outputs)?;
4363
4364        // Execute - if this fails, we haven't modified self yet, so state is safe
4365        let mock_config = self.sketch_mock_config(sketch_id, true, Default::default(), false);
4366        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
4367
4368        let new_object_ids = {
4369            // Extract the constraint ID from the execution outcome using source_range_to_object
4370            let constraint_id = outcome
4371                .source_range_to_object
4372                .get(&constraint_node_ref.range)
4373                .copied()
4374                .ok_or_else(|| {
4375                    KclErrorWithOutputs::from_error_outcome(
4376                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4377                        outcome.clone(),
4378                    )
4379                })?;
4380            vec![constraint_id]
4381        };
4382
4383        // Only now, after all operations succeeded, update self.program.
4384        // This ensures state is only modified if everything succeeds.
4385        self.program = new_program;
4386
4387        // Uses MockConfig::default() which has freedom_analysis: true
4388        let outcome = self.update_state_after_exec(outcome, true);
4389        self.replace_sketch_var_warm_starts(sketch_id, &outcome);
4390
4391        let src_delta = SourceDelta { text: new_source };
4392        let scene_graph_delta = SceneGraphDelta {
4393            new_graph: self.scene_graph.clone(),
4394            invalidates_ids: false,
4395            new_objects: new_object_ids,
4396            exec_outcome: outcome,
4397        };
4398        Ok((src_delta, scene_graph_delta))
4399    }
4400
4401    // Find constraints that reference the given segments.
4402    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4403        if segment_ids_set.contains(&segment_id) {
4404            return true;
4405        }
4406
4407        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4408            return false;
4409        };
4410        let ObjectKind::Segment { segment } = &segment_object.kind else {
4411            return false;
4412        };
4413        let Segment::Point(point) = segment else {
4414            return false;
4415        };
4416
4417        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4418    }
4419
4420    fn remaining_constraint_segments(
4421        &self,
4422        segments: &[ConstraintSegment],
4423        segment_ids_set: &AhashIndexSet<ObjectId>,
4424    ) -> Vec<ConstraintSegment> {
4425        segments
4426            .iter()
4427            .copied()
4428            .filter(|segment| match segment {
4429                ConstraintSegment::Origin(_) => true,
4430                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4431            })
4432            .collect()
4433    }
4434
4435    fn find_referenced_constraints(
4436        &self,
4437        sketch_id: ObjectId,
4438        segment_ids_set: &AhashIndexSet<ObjectId>,
4439    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4440        // Look up the sketch.
4441        let sketch_object = self
4442            .scene_graph
4443            .objects
4444            .get(sketch_id.0)
4445            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4446        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4447            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4448        };
4449        let mut constraint_ids_set = AhashIndexSet::default();
4450        for constraint_id in &sketch.constraints {
4451            let constraint_object = self
4452                .scene_graph
4453                .objects
4454                .get(constraint_id.0)
4455                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4456            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4457                return Err(KclError::refactor(format!(
4458                    "Object is not a constraint, it is {}",
4459                    constraint_object.kind.human_friendly_kind_with_article()
4460                )));
4461            };
4462            let depends_on_segment = match constraint {
4463                Constraint::Coincident(c) => c
4464                    .segment_ids()
4465                    .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4466                Constraint::Distance(d) => d
4467                    .point_ids()
4468                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4469                Constraint::Fixed(fixed) => fixed
4470                    .points
4471                    .iter()
4472                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4473                Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4474                Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4475                Constraint::EqualRadius(equal_radius) => equal_radius
4476                    .input
4477                    .iter()
4478                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4479                Constraint::HorizontalDistance(d) => d
4480                    .point_ids()
4481                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4482                Constraint::VerticalDistance(d) => d
4483                    .point_ids()
4484                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4485                Constraint::Horizontal(h) => match h {
4486                    Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4487                    Horizontal::Points { points } => points.iter().any(|point| match point {
4488                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4489                        ConstraintSegment::Origin(_) => false,
4490                    }),
4491                },
4492                Constraint::Vertical(v) => match v {
4493                    Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4494                    Vertical::Points { points } => points.iter().any(|point| match point {
4495                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4496                        ConstraintSegment::Origin(_) => false,
4497                    }),
4498                },
4499                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4500                    .lines
4501                    .iter()
4502                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4503                Constraint::Midpoint(midpoint) => {
4504                    self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4505                        || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4506                }
4507                Constraint::Parallel(parallel) => parallel
4508                    .lines
4509                    .iter()
4510                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4511                Constraint::Perpendicular(perpendicular) => perpendicular
4512                    .lines
4513                    .iter()
4514                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4515                Constraint::Angle(angle) => angle
4516                    .lines
4517                    .iter()
4518                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4519                Constraint::Symmetric(symmetric) => {
4520                    self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4521                        || symmetric
4522                            .input
4523                            .iter()
4524                            .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4525                }
4526                Constraint::Tangent(tangent) => tangent
4527                    .input
4528                    .iter()
4529                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4530            };
4531            if depends_on_segment {
4532                constraint_ids_set.insert(*constraint_id);
4533            }
4534        }
4535        Ok(constraint_ids_set)
4536    }
4537
4538    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4539        let mut outcome = outcome;
4540        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4541
4542        if freedom_analysis_ran {
4543            // When freedom analysis ran, replace the cache entirely with new values
4544            // Don't merge with old values since IDs might have changed
4545            self.point_freedom_cache.clear();
4546            for new_obj in &new_objects {
4547                if let ObjectKind::Segment {
4548                    segment: crate::front::Segment::Point(point),
4549                } = &new_obj.kind
4550                {
4551                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4552                }
4553            }
4554            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4555            // Objects are already correct from the analysis, just use them as-is
4556            self.scene_graph.objects = new_objects;
4557        } else {
4558            // When freedom analysis didn't run, preserve old values and merge
4559            // Before replacing objects, extract and store freedom values from old objects
4560            for old_obj in &self.scene_graph.objects {
4561                if let ObjectKind::Segment {
4562                    segment: crate::front::Segment::Point(point),
4563                } = &old_obj.kind
4564                {
4565                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4566                }
4567            }
4568
4569            // Update objects, preserving stored freedom values when new is Free (might be default)
4570            let mut updated_objects = Vec::with_capacity(new_objects.len());
4571            for new_obj in new_objects {
4572                let mut obj = new_obj;
4573                if let ObjectKind::Segment {
4574                    segment: crate::front::Segment::Point(point),
4575                } = &mut obj.kind
4576                {
4577                    let new_freedom = point.freedom;
4578                    // When freedom_analysis=false, new values are defaults (Free).
4579                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4580                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4581                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4582                    // when there are actually unsatisfied constraints.
4583                    match new_freedom {
4584                        Freedom::Free => {
4585                            match self.point_freedom_cache.get(&obj.id).copied() {
4586                                Some(Freedom::Conflict) => {
4587                                    // Don't preserve Conflict - conflicts are transient
4588                                    // Keep it as Free
4589                                }
4590                                Some(Freedom::Fixed) => {
4591                                    // Preserve Fixed cached value
4592                                    point.freedom = Freedom::Fixed;
4593                                }
4594                                Some(Freedom::Free) => {
4595                                    // If stored is also Free, keep Free (no change needed)
4596                                }
4597                                None => {
4598                                    // If no cached value, keep Free (default)
4599                                }
4600                            }
4601                        }
4602                        Freedom::Fixed => {
4603                            // Use new value (already set)
4604                        }
4605                        Freedom::Conflict => {
4606                            // Use new value (already set)
4607                        }
4608                    }
4609                    // Store the new freedom value (even if it's Free, so we know it was set)
4610                    self.point_freedom_cache.insert(obj.id, point.freedom);
4611                }
4612                updated_objects.push(obj);
4613            }
4614
4615            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4616            self.scene_graph.objects = updated_objects;
4617        }
4618        outcome
4619    }
4620
4621    fn mutate_ast(
4622        &mut self,
4623        ast: &mut ast::Node<ast::Program>,
4624        object_id: ObjectId,
4625        command: AstMutateCommand,
4626    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4627        let sketch_object = self
4628            .scene_graph
4629            .objects
4630            .get(object_id.0)
4631            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4632        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4633    }
4634}
4635
4636fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4637    // Look up existing sketch.
4638    let sketch_object = scene_graph
4639        .objects
4640        .get(sketch_id.0)
4641        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4642    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4643        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4644    };
4645    expect_single_node_ref(sketch_object)
4646}
4647
4648fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4649    match &object.source {
4650        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4651            range: *range,
4652            node_path: node_path.clone(),
4653        }),
4654        SourceRef::BackTrace { ranges } => {
4655            let [range] = ranges.as_slice() else {
4656                return Err(KclError::refactor(format!(
4657                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4658                    ranges.len()
4659                )));
4660            };
4661            Ok(AstNodeRef {
4662                range: range.0,
4663                node_path: range.1.clone(),
4664            })
4665        }
4666    }
4667}
4668
4669/// This is a deprecated fall-back implementation. Prefer
4670/// [`only_sketch_block()`] to avoid reliance on source ranges.
4671fn only_sketch_block_from_range(
4672    ast: &mut ast::Node<ast::Program>,
4673    sketch_block_range: SourceRange,
4674    edit_kind: ChangeKind,
4675) -> Result<(), KclError> {
4676    let r1 = sketch_block_range;
4677    let matches_range = |r2: SourceRange| -> bool {
4678        // We may have added items to the sketch block, so the end may not be an
4679        // exact match.
4680        match edit_kind {
4681            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4682            // For edit, we don't know whether it grew or shrank.
4683            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4684            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4685            // No edit should be an exact match.
4686            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4687        }
4688    };
4689    let mut found = false;
4690    for item in ast.body.iter_mut() {
4691        match item {
4692            ast::BodyItem::ImportStatement(_) => {}
4693            ast::BodyItem::ExpressionStatement(node) => {
4694                if matches_range(SourceRange::from(&*node))
4695                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4696                {
4697                    sketch_block.is_being_edited = true;
4698                    found = true;
4699                    break;
4700                }
4701            }
4702            ast::BodyItem::VariableDeclaration(node) => {
4703                if matches_range(SourceRange::from(&node.declaration.init))
4704                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4705                {
4706                    sketch_block.is_being_edited = true;
4707                    found = true;
4708                    break;
4709                }
4710            }
4711            ast::BodyItem::TypeDeclaration(_) => {}
4712            ast::BodyItem::ReturnStatement(node) => {
4713                if matches_range(SourceRange::from(&node.argument))
4714                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4715                {
4716                    sketch_block.is_being_edited = true;
4717                    found = true;
4718                    break;
4719                }
4720            }
4721        }
4722    }
4723    if !found {
4724        return Err(KclError::refactor(format!(
4725            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4726        )));
4727    }
4728
4729    Ok(())
4730}
4731
4732fn only_sketch_block(
4733    ast: &mut ast::Node<ast::Program>,
4734    sketch_block_ref: &AstNodeRef,
4735    edit_kind: ChangeKind,
4736) -> Result<(), KclError> {
4737    let Some(target_node_path) = &sketch_block_ref.node_path else {
4738        #[cfg(target_arch = "wasm32")]
4739        web_sys::console::warn_1(
4740            &format!(
4741                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4742                &sketch_block_ref
4743            )
4744            .into(),
4745        );
4746        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4747    };
4748    let mut found = false;
4749    for item in ast.body.iter_mut() {
4750        match item {
4751            ast::BodyItem::ImportStatement(_) => {}
4752            ast::BodyItem::ExpressionStatement(node) => {
4753                // Check the statement.
4754                if let Some(node_path) = &node.node_path
4755                    && node_path == target_node_path
4756                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4757                {
4758                    sketch_block.is_being_edited = true;
4759                    found = true;
4760                    break;
4761                }
4762                // Check the expression.
4763                if let Some(node_path) = node.expression.node_path()
4764                    && node_path == target_node_path
4765                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4766                {
4767                    sketch_block.is_being_edited = true;
4768                    found = true;
4769                    break;
4770                }
4771            }
4772            ast::BodyItem::VariableDeclaration(node) => {
4773                if let Some(node_path) = node.declaration.init.node_path()
4774                    && node_path == target_node_path
4775                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4776                {
4777                    sketch_block.is_being_edited = true;
4778                    found = true;
4779                    break;
4780                }
4781            }
4782            ast::BodyItem::TypeDeclaration(_) => {}
4783            ast::BodyItem::ReturnStatement(node) => {
4784                if let Some(node_path) = node.argument.node_path()
4785                    && node_path == target_node_path
4786                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4787                {
4788                    sketch_block.is_being_edited = true;
4789                    found = true;
4790                    break;
4791                }
4792            }
4793        }
4794    }
4795    if !found {
4796        return Err(KclError::refactor(format!(
4797            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4798        )));
4799    }
4800
4801    Ok(())
4802}
4803
4804fn sketch_on_ast_expr(
4805    ast: &mut ast::Node<ast::Program>,
4806    scene_graph: &SceneGraph,
4807    on: &Plane,
4808) -> Result<ast::Expr, KclError> {
4809    match on {
4810        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4811        Plane::Object(object_id) => {
4812            let on_object = scene_graph
4813                .objects
4814                .get(object_id.0)
4815                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4816            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4817                return Ok(face_expr);
4818            }
4819            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4820        }
4821    }
4822}
4823
4824fn sketch_face_of_scene_object_ast_expr(
4825    ast: &mut ast::Node<ast::Program>,
4826    on_object: &crate::front::Object,
4827) -> Result<Option<ast::Expr>, KclError> {
4828    let SourceRef::BackTrace { ranges } = &on_object.source else {
4829        return Ok(None);
4830    };
4831
4832    match &on_object.kind {
4833        ObjectKind::Wall(_) => {
4834            let [sweep_range, segment_range] = ranges.as_slice() else {
4835                return Err(KclError::refactor(format!(
4836                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4837                    ranges.len(),
4838                    on_object.artifact_id
4839                )));
4840            };
4841            let sweep_ref = get_or_insert_ast_reference(
4842                ast,
4843                &SourceRef::Simple {
4844                    range: sweep_range.0,
4845                    node_path: sweep_range.1.clone(),
4846                },
4847                "solid",
4848                None,
4849            )?;
4850            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4851                return Err(KclError::refactor(format!(
4852                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4853                    on_object.artifact_id
4854                )));
4855            };
4856            let solid_name = solid_name_expr.name.name.clone();
4857            let solid_expr = ast_name_expr(solid_name.clone());
4858            let segment_ref = get_or_insert_ast_reference(
4859                ast,
4860                &SourceRef::Simple {
4861                    range: segment_range.0,
4862                    node_path: segment_range.1.clone(),
4863                },
4864                LINE_VARIABLE,
4865                None,
4866            )?;
4867
4868            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4869                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4870                    return Err(KclError::refactor(format!(
4871                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4872                        on_object.artifact_id
4873                    )));
4874                };
4875                create_member_expression(
4876                    create_member_expression(ast_name_expr(region_name), "tags"),
4877                    &segment_name_expr.name.name,
4878                )
4879            } else {
4880                segment_ref
4881            };
4882
4883            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4884        }
4885        ObjectKind::Cap(cap) => {
4886            let [range] = ranges.as_slice() else {
4887                return Err(KclError::refactor(format!(
4888                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4889                    ranges.len(),
4890                    on_object.artifact_id
4891                )));
4892            };
4893            let sweep_ref = get_or_insert_ast_reference(
4894                ast,
4895                &SourceRef::Simple {
4896                    range: range.0,
4897                    node_path: range.1.clone(),
4898                },
4899                "solid",
4900                None,
4901            )?;
4902            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4903                return Err(KclError::refactor(format!(
4904                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4905                    on_object.artifact_id
4906                )));
4907            };
4908            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4909            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4910            let face_expr = match cap.kind {
4911                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4912                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4913            };
4914
4915            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4916        }
4917        _ => Ok(None),
4918    }
4919}
4920
4921fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4922    let mut existing_artifact_ids = scene_objects
4923        .iter()
4924        .map(|object| object.artifact_id)
4925        .collect::<HashSet<_>>();
4926
4927    for artifact in artifact_graph.values() {
4928        match artifact {
4929            Artifact::Wall(wall) => {
4930                if existing_artifact_ids.contains(&wall.id) {
4931                    continue;
4932                }
4933
4934                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4935                    Artifact::Segment(segment) => Some(segment),
4936                    _ => None,
4937                }) else {
4938                    continue;
4939                };
4940                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4941                    Artifact::Sweep(sweep) => Some(sweep),
4942                    _ => None,
4943                }) else {
4944                    continue;
4945                };
4946                let source_segment = segment
4947                    .original_seg_id
4948                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4949                    .and_then(|artifact| match artifact {
4950                        Artifact::Segment(segment) => Some(segment),
4951                        _ => None,
4952                    })
4953                    .unwrap_or(segment);
4954                let id = ObjectId(scene_objects.len());
4955                scene_objects.push(crate::front::Object {
4956                    id,
4957                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4958                    label: Default::default(),
4959                    comments: Default::default(),
4960                    artifact_id: wall.id,
4961                    source: SourceRef::BackTrace {
4962                        ranges: vec![
4963                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4964                            (
4965                                source_segment.code_ref.range,
4966                                Some(source_segment.code_ref.node_path.clone()),
4967                            ),
4968                        ],
4969                    },
4970                });
4971                existing_artifact_ids.insert(wall.id);
4972            }
4973            Artifact::Cap(cap) => {
4974                if existing_artifact_ids.contains(&cap.id) {
4975                    continue;
4976                }
4977
4978                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4979                    Artifact::Sweep(sweep) => Some(sweep),
4980                    _ => None,
4981                }) else {
4982                    continue;
4983                };
4984                let id = ObjectId(scene_objects.len());
4985                let kind = match cap.sub_type {
4986                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4987                    CapSubType::End => crate::frontend::api::CapKind::End,
4988                };
4989                scene_objects.push(crate::front::Object {
4990                    id,
4991                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4992                    label: Default::default(),
4993                    comments: Default::default(),
4994                    artifact_id: cap.id,
4995                    source: SourceRef::BackTrace {
4996                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4997                    },
4998                });
4999                existing_artifact_ids.insert(cap.id);
5000            }
5001            _ => {}
5002        }
5003    }
5004}
5005
5006fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5007    use crate::engine::PlaneName;
5008
5009    match name {
5010        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5011        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5012        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5013        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5014        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5015        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5016    }
5017}
5018
5019fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5020    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5021        ast::UnaryOperator::Neg,
5022        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5023    )))
5024}
5025
5026fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5027    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5028        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5029        unlabeled: Some(solid_expr),
5030        arguments: vec![ast::LabeledArg {
5031            label: Some(ast::Identifier::new("face")),
5032            arg: face_expr,
5033        }],
5034        digest: None,
5035        non_code_meta: Default::default(),
5036    })))
5037}
5038
5039fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5040    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5041        return None;
5042    };
5043    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5044        return None;
5045    };
5046    if !matches!(
5047        sweep_call.callee.name.name.as_str(),
5048        "extrude" | "revolve" | "sweep" | "loft"
5049    ) {
5050        return None;
5051    }
5052    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5053        return None;
5054    };
5055    let candidate = region_name_expr.name.name.clone();
5056    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5057        return None;
5058    };
5059    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5060        return None;
5061    };
5062    if region_call.callee.name.name != "region" {
5063        return None;
5064    }
5065    Some(candidate)
5066}
5067
5068/// Return the AST expression referencing the variable at the given source ref.
5069/// If no such variable exists, insert a new variable declaration with the given
5070/// prefix.
5071///
5072/// This may return a complex expression referencing properties of the variable
5073/// (e.g., `line1.start`).
5074fn get_or_insert_ast_reference(
5075    ast: &mut ast::Node<ast::Program>,
5076    source_ref: &SourceRef,
5077    prefix: &str,
5078    property: Option<&str>,
5079) -> Result<ast::Expr, KclError> {
5080    let command = AstMutateCommand::AddVariableDeclaration {
5081        prefix: prefix.to_owned(),
5082    };
5083    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5084    let AstMutateCommandReturn::Name(var_name) = ret else {
5085        return Err(KclError::refactor(
5086            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5087        ));
5088    };
5089    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5090    let Some(property) = property else {
5091        // No property; just return the variable name.
5092        return Ok(var_expr);
5093    };
5094
5095    Ok(create_member_expression(var_expr, property))
5096}
5097
5098fn mutate_ast_node_by_source_ref(
5099    ast: &mut ast::Node<ast::Program>,
5100    source_ref: &SourceRef,
5101    command: AstMutateCommand,
5102) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5103    let (source_range, node_path) = match source_ref {
5104        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5105        SourceRef::BackTrace { ranges } => {
5106            let [range] = ranges.as_slice() else {
5107                return Err(KclError::refactor(format!(
5108                    "Expected single source ref, got {}; ranges={ranges:#?}",
5109                    ranges.len(),
5110                )));
5111            };
5112            (range.0, range.1.clone())
5113        }
5114    };
5115    let mut context = AstMutateContext {
5116        source_range,
5117        node_path,
5118        command,
5119        defined_names_stack: Default::default(),
5120    };
5121    let control = dfs_mut(ast, &mut context);
5122    match control {
5123        ControlFlow::Continue(_) => Err(KclError::refactor(
5124            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5125        )),
5126        ControlFlow::Break(break_value) => break_value,
5127    }
5128}
5129
5130#[derive(Debug)]
5131struct AstMutateContext {
5132    source_range: SourceRange,
5133    node_path: Option<ast::NodePath>,
5134    command: AstMutateCommand,
5135    defined_names_stack: Vec<HashSet<String>>,
5136}
5137
5138#[derive(Debug)]
5139#[allow(clippy::large_enum_variant)]
5140enum AstMutateCommand {
5141    /// Add an expression statement to the sketch block.
5142    AddSketchBlockExprStmt {
5143        expr: ast::Expr,
5144    },
5145    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5146    AddSketchBlockVarDecl {
5147        prefix: String,
5148        expr: ast::Expr,
5149    },
5150    AddVariableDeclaration {
5151        prefix: String,
5152    },
5153    EditPoint {
5154        at: ast::Expr,
5155    },
5156    EditLine {
5157        start: ast::Expr,
5158        end: ast::Expr,
5159        construction: Option<bool>,
5160    },
5161    EditArc {
5162        start: ast::Expr,
5163        end: ast::Expr,
5164        center: ast::Expr,
5165        construction: Option<bool>,
5166    },
5167    EditCircle {
5168        start: ast::Expr,
5169        center: ast::Expr,
5170        construction: Option<bool>,
5171    },
5172    EditConstraintValue {
5173        value: ast::BinaryPart,
5174    },
5175    EditDistanceConstraintLabelPosition {
5176        label_position: ast::Expr,
5177    },
5178    EditCallUnlabeled {
5179        arg: ast::Expr,
5180    },
5181    EditVarInitialValue {
5182        value: Number,
5183    },
5184    DeleteNode,
5185}
5186
5187impl AstMutateCommand {
5188    fn needs_defined_names_stack(&self) -> bool {
5189        matches!(
5190            self,
5191            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5192        )
5193    }
5194}
5195
5196#[derive(Debug)]
5197enum AstMutateCommandReturn {
5198    None,
5199    Name(String),
5200}
5201
5202#[derive(Debug, Clone)]
5203struct AstNodeRef {
5204    range: SourceRange,
5205    node_path: Option<ast::NodePath>,
5206}
5207
5208impl<T> From<&ast::Node<T>> for AstNodeRef {
5209    fn from(value: &ast::Node<T>) -> Self {
5210        AstNodeRef {
5211            range: value.into(),
5212            node_path: value.node_path.clone(),
5213        }
5214    }
5215}
5216
5217impl From<&ast::BodyItem> for AstNodeRef {
5218    fn from(value: &ast::BodyItem) -> Self {
5219        match value {
5220            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5221                range: node.into(),
5222                node_path: node.node_path.clone(),
5223            },
5224            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5225                range: node.into(),
5226                node_path: node.node_path.clone(),
5227            },
5228            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5229                range: node.into(),
5230                node_path: node.node_path.clone(),
5231            },
5232            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5233                range: node.into(),
5234                node_path: node.node_path.clone(),
5235            },
5236            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5237                range: node.into(),
5238                node_path: node.node_path.clone(),
5239            },
5240        }
5241    }
5242}
5243
5244impl From<&ast::Expr> for AstNodeRef {
5245    fn from(value: &ast::Expr) -> Self {
5246        AstNodeRef {
5247            range: SourceRange::from(value),
5248            node_path: value.node_path().cloned(),
5249        }
5250    }
5251}
5252
5253impl From<&AstMutateContext> for AstNodeRef {
5254    fn from(value: &AstMutateContext) -> Self {
5255        AstNodeRef {
5256            range: value.source_range,
5257            node_path: value.node_path.clone(),
5258        }
5259    }
5260}
5261
5262impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5263    type Error = crate::walk::AstNodeError;
5264
5265    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5266        Ok(AstNodeRef {
5267            range: SourceRange::try_from(value)?,
5268            node_path: value.try_into()?,
5269        })
5270    }
5271}
5272
5273impl From<AstNodeRef> for SourceRange {
5274    fn from(value: AstNodeRef) -> Self {
5275        value.range
5276    }
5277}
5278
5279impl Visitor for AstMutateContext {
5280    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5281    type Continue = ();
5282
5283    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5284        filter_and_process(self, node)
5285    }
5286
5287    fn finish(&mut self, node: NodeMut<'_>) {
5288        match &node {
5289            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5290                self.defined_names_stack.pop();
5291            }
5292            _ => {}
5293        }
5294    }
5295}
5296
5297fn filter_and_process(
5298    ctx: &mut AstMutateContext,
5299    node: NodeMut,
5300) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5301    let Ok(node_range) = SourceRange::try_from(&node) else {
5302        // Nodes that can't be converted to a range aren't interesting.
5303        return TraversalReturn::new_continue(());
5304    };
5305    // If we're adding a variable declaration, we need to look at variable
5306    // declaration expressions to see if it already has a variable, before
5307    // continuing. The variable declaration's source range won't match the
5308    // target; its init expression will.
5309    if let NodeMut::VariableDeclaration(var_decl) = &node {
5310        let expr_range = SourceRange::from(&var_decl.declaration.init);
5311        let expr_node_path = var_decl.declaration.init.node_path();
5312        if source_ref_matches(ctx, expr_range, expr_node_path) {
5313            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5314                // We found the variable declaration expression. It doesn't need
5315                // to be added.
5316                return TraversalReturn::new_break(Ok((
5317                    AstNodeRef::from(&**var_decl),
5318                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5319                )));
5320            }
5321            if let AstMutateCommand::DeleteNode = &ctx.command {
5322                // We found the variable declaration. Delete the variable along
5323                // with the segment.
5324                return TraversalReturn {
5325                    mutate_body_item: MutateBodyItem::Delete,
5326                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5327                };
5328            }
5329        }
5330    }
5331    // Similar thing with expression statement. We need to look at the
5332    // expression inside it.
5333    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5334        let expr_range = SourceRange::from(&expr_stmt.expression);
5335        let expr_node_path = expr_stmt.expression.node_path();
5336        if source_ref_matches(ctx, expr_range, expr_node_path) {
5337            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5338                // We found the node wrapped in an expression statement. Process
5339                // the statement.
5340                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5341                    return TraversalReturn::new_continue(());
5342                };
5343                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5344            }
5345            if let AstMutateCommand::DeleteNode = &ctx.command {
5346                // We found the node wrapped in an expression statement. Delete
5347                // the whole statement.
5348                return TraversalReturn {
5349                    mutate_body_item: MutateBodyItem::Delete,
5350                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5351                };
5352            }
5353        }
5354    }
5355
5356    if ctx.command.needs_defined_names_stack() {
5357        if let NodeMut::Program(program) = &node {
5358            ctx.defined_names_stack.push(find_defined_names(*program));
5359        } else if let NodeMut::SketchBlock(block) = &node {
5360            ctx.defined_names_stack.push(find_defined_names(&block.body));
5361        }
5362    }
5363
5364    // Make sure the node matches the source ref.
5365    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5366    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5367        return TraversalReturn::new_continue(());
5368    }
5369    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5370        return TraversalReturn::new_continue(());
5371    };
5372    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5373}
5374
5375fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5376    match &ctx.node_path {
5377        Some(target) => Some(target) == node_path,
5378        None => node_range == ctx.source_range,
5379    }
5380}
5381
5382fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5383    match &ctx.command {
5384        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5385            if let NodeMut::SketchBlock(sketch_block) = node {
5386                sketch_block
5387                    .body
5388                    .items
5389                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5390                        inner: ast::ExpressionStatement {
5391                            expression: expr.clone(),
5392                            digest: None,
5393                        },
5394                        start: Default::default(),
5395                        end: Default::default(),
5396                        module_id: Default::default(),
5397                        node_path: None,
5398                        outer_attrs: Default::default(),
5399                        pre_comments: Default::default(),
5400                        comment_start: Default::default(),
5401                    }));
5402                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5403            }
5404        }
5405        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5406            if let NodeMut::SketchBlock(sketch_block) = node {
5407                let empty_defined_names = HashSet::new();
5408                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5409                let Ok(name) = next_free_name(prefix, defined_names) else {
5410                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5411                };
5412                sketch_block
5413                    .body
5414                    .items
5415                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5416                        ast::VariableDeclaration::new(
5417                            ast::VariableDeclarator::new(&name, expr.clone()),
5418                            ast::ItemVisibility::Default,
5419                            ast::VariableKind::Const,
5420                        ),
5421                    ))));
5422                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5423            }
5424        }
5425        AstMutateCommand::AddVariableDeclaration { prefix } => {
5426            if let NodeMut::VariableDeclaration(inner) = node {
5427                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5428            }
5429            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5430                let empty_defined_names = HashSet::new();
5431                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5432                let Ok(name) = next_free_name(prefix, defined_names) else {
5433                    // TODO: Return an error instead?
5434                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5435                };
5436                let mutate_node =
5437                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5438                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5439                        ast::ItemVisibility::Default,
5440                        ast::VariableKind::Const,
5441                    ))));
5442                return TraversalReturn {
5443                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5444                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5445                };
5446            }
5447        }
5448        AstMutateCommand::EditPoint { at } => {
5449            if let NodeMut::CallExpressionKw(call) = node {
5450                if call.callee.name.name != POINT_FN {
5451                    return TraversalReturn::new_continue(());
5452                }
5453                // Update the arguments.
5454                for labeled_arg in &mut call.arguments {
5455                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5456                        labeled_arg.arg = at.clone();
5457                    }
5458                }
5459                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5460            }
5461        }
5462        AstMutateCommand::EditLine {
5463            start,
5464            end,
5465            construction,
5466        } => {
5467            if let NodeMut::CallExpressionKw(call) = node {
5468                if call.callee.name.name != LINE_FN {
5469                    return TraversalReturn::new_continue(());
5470                }
5471                // Update the arguments.
5472                for labeled_arg in &mut call.arguments {
5473                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5474                        labeled_arg.arg = start.clone();
5475                    }
5476                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5477                        labeled_arg.arg = end.clone();
5478                    }
5479                }
5480                // Handle construction kwarg
5481                if let Some(construction_value) = construction {
5482                    let construction_exists = call
5483                        .arguments
5484                        .iter()
5485                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5486                    if *construction_value {
5487                        // Add or update construction=true
5488                        if construction_exists {
5489                            // Update existing construction kwarg
5490                            for labeled_arg in &mut call.arguments {
5491                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5492                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5493                                        value: ast::LiteralValue::Bool(true),
5494                                        raw: "true".to_string(),
5495                                        digest: None,
5496                                    })));
5497                                }
5498                            }
5499                        } else {
5500                            // Add new construction kwarg
5501                            call.arguments.push(ast::LabeledArg {
5502                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5503                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5504                                    value: ast::LiteralValue::Bool(true),
5505                                    raw: "true".to_string(),
5506                                    digest: None,
5507                                }))),
5508                            });
5509                        }
5510                    } else {
5511                        // Remove construction kwarg if it exists
5512                        call.arguments
5513                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5514                    }
5515                }
5516                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5517            }
5518        }
5519        AstMutateCommand::EditArc {
5520            start,
5521            end,
5522            center,
5523            construction,
5524        } => {
5525            if let NodeMut::CallExpressionKw(call) = node {
5526                if call.callee.name.name != ARC_FN {
5527                    return TraversalReturn::new_continue(());
5528                }
5529                // Update the arguments.
5530                for labeled_arg in &mut call.arguments {
5531                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5532                        labeled_arg.arg = start.clone();
5533                    }
5534                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5535                        labeled_arg.arg = end.clone();
5536                    }
5537                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5538                        labeled_arg.arg = center.clone();
5539                    }
5540                }
5541                // Handle construction kwarg
5542                if let Some(construction_value) = construction {
5543                    let construction_exists = call
5544                        .arguments
5545                        .iter()
5546                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5547                    if *construction_value {
5548                        // Add or update construction=true
5549                        if construction_exists {
5550                            // Update existing construction kwarg
5551                            for labeled_arg in &mut call.arguments {
5552                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5553                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5554                                        value: ast::LiteralValue::Bool(true),
5555                                        raw: "true".to_string(),
5556                                        digest: None,
5557                                    })));
5558                                }
5559                            }
5560                        } else {
5561                            // Add new construction kwarg
5562                            call.arguments.push(ast::LabeledArg {
5563                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5564                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5565                                    value: ast::LiteralValue::Bool(true),
5566                                    raw: "true".to_string(),
5567                                    digest: None,
5568                                }))),
5569                            });
5570                        }
5571                    } else {
5572                        // Remove construction kwarg if it exists
5573                        call.arguments
5574                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5575                    }
5576                }
5577                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5578            }
5579        }
5580        AstMutateCommand::EditCircle {
5581            start,
5582            center,
5583            construction,
5584        } => {
5585            if let NodeMut::CallExpressionKw(call) = node {
5586                if call.callee.name.name != CIRCLE_FN {
5587                    return TraversalReturn::new_continue(());
5588                }
5589                // Update the arguments.
5590                for labeled_arg in &mut call.arguments {
5591                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5592                        labeled_arg.arg = start.clone();
5593                    }
5594                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5595                        labeled_arg.arg = center.clone();
5596                    }
5597                }
5598                // Handle construction kwarg
5599                if let Some(construction_value) = construction {
5600                    let construction_exists = call
5601                        .arguments
5602                        .iter()
5603                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5604                    if *construction_value {
5605                        if construction_exists {
5606                            for labeled_arg in &mut call.arguments {
5607                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5608                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5609                                        value: ast::LiteralValue::Bool(true),
5610                                        raw: "true".to_string(),
5611                                        digest: None,
5612                                    })));
5613                                }
5614                            }
5615                        } else {
5616                            call.arguments.push(ast::LabeledArg {
5617                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5618                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5619                                    value: ast::LiteralValue::Bool(true),
5620                                    raw: "true".to_string(),
5621                                    digest: None,
5622                                }))),
5623                            });
5624                        }
5625                    } else {
5626                        call.arguments
5627                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5628                    }
5629                }
5630                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5631            }
5632        }
5633        AstMutateCommand::EditConstraintValue { value } => {
5634            if let NodeMut::BinaryExpression(binary_expr) = node {
5635                let left_is_constraint = matches!(
5636                    &binary_expr.left,
5637                    ast::BinaryPart::CallExpressionKw(call)
5638                        if matches!(
5639                            call.callee.name.name.as_str(),
5640                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5641                        )
5642                );
5643                if left_is_constraint {
5644                    binary_expr.right = value.clone();
5645                } else {
5646                    binary_expr.left = value.clone();
5647                }
5648
5649                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5650            }
5651        }
5652        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5653            if let NodeMut::BinaryExpression(binary_expr) = node {
5654                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5655                    return TraversalReturn::new_continue(());
5656                };
5657                if !matches!(
5658                    call.callee.name.name.as_str(),
5659                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5660                ) {
5661                    return TraversalReturn::new_continue(());
5662                }
5663
5664                if let Some(label_arg) = call
5665                    .arguments
5666                    .iter_mut()
5667                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5668                {
5669                    label_arg.arg = label_position.clone();
5670                } else {
5671                    call.arguments.push(ast::LabeledArg {
5672                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5673                        arg: label_position.clone(),
5674                    });
5675                }
5676
5677                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5678            }
5679        }
5680        AstMutateCommand::EditCallUnlabeled { arg } => {
5681            if let NodeMut::CallExpressionKw(call) = node {
5682                call.unlabeled = Some(arg.clone());
5683                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5684            }
5685        }
5686        AstMutateCommand::EditVarInitialValue { value } => {
5687            if let NodeMut::NumericLiteral(numeric_literal) = node {
5688                // Update the initial value.
5689                let Ok(literal) = to_source_number(*value) else {
5690                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5691                        "Could not convert number to AST literal: {:?}",
5692                        *value
5693                    ))));
5694                };
5695                *numeric_literal = ast::Node::no_src(literal);
5696                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5697            }
5698        }
5699        AstMutateCommand::DeleteNode => {
5700            return TraversalReturn {
5701                mutate_body_item: MutateBodyItem::Delete,
5702                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5703            };
5704        }
5705    }
5706    TraversalReturn::new_continue(())
5707}
5708
5709struct FindSketchBlockSourceRange {
5710    /// The source range of the sketch block before mutation.
5711    target_before_mutation: SourceRange,
5712    /// The source range of the sketch block's last body item after mutation. We
5713    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5714    /// shared reference.
5715    found: Cell<Option<AstNodeRef>>,
5716}
5717
5718impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5719    type Error = crate::front::Error;
5720
5721    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5722        let Ok(node_range) = SourceRange::try_from(&node) else {
5723            return Ok(true);
5724        };
5725
5726        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5727            if node_range.module_id() == self.target_before_mutation.module_id()
5728                && node_range.start() == self.target_before_mutation.start()
5729                // End shouldn't match since we added something.
5730                && node_range.end() >= self.target_before_mutation.end()
5731            {
5732                self.found.set(sketch_block.body.items.last().map(|item| match item {
5733                    // For declarations like `circle1 = circle(...)`, use
5734                    // the init expression range so lookup in source_range_to_object
5735                    // matches the segment source range.
5736                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5737                    _ => AstNodeRef::from(item),
5738                }));
5739                return Ok(false);
5740            } else {
5741                // We found a different sketch block. No need to descend into
5742                // its children since sketch blocks cannot be nested.
5743                return Ok(true);
5744            }
5745        }
5746
5747        for child in node.children().iter() {
5748            if !child.visit(*self)? {
5749                return Ok(false);
5750            }
5751        }
5752
5753        Ok(true)
5754    }
5755}
5756
5757struct FindSketchBlockByNodePath {
5758    /// The Node Path of the sketch block before mutation.
5759    target_node_path: ast::NodePath,
5760    /// The ref of the sketch block's last body item after mutation. We need to
5761    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5762    /// reference.
5763    found: Cell<Option<AstNodeRef>>,
5764}
5765
5766impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5767    type Error = crate::front::Error;
5768
5769    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5770        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5771            return Ok(true);
5772        };
5773
5774        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5775            if let Some(node_path) = node_path
5776                && node_path == self.target_node_path
5777            {
5778                self.found.set(sketch_block.body.items.last().map(|item| match item {
5779                    // For declarations like `circle1 = circle(...)`, use
5780                    // the init expression range so lookup in source_range_to_object
5781                    // matches the segment source range.
5782                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5783                    _ => AstNodeRef::from(item),
5784                }));
5785
5786                return Ok(false);
5787            } else {
5788                // We found a different sketch block. No need to descend into
5789                // its children since sketch blocks cannot be nested.
5790                return Ok(true);
5791            }
5792        }
5793
5794        for child in node.children().iter() {
5795            if !child.visit(*self)? {
5796                return Ok(false);
5797            }
5798        }
5799
5800        Ok(true)
5801    }
5802}
5803
5804/// After adding an item to a sketch block, find the sketch block, and get the
5805/// source range of the added item. We assume that the added item is the last
5806/// item in the sketch block and that the sketch block's source range has grown,
5807/// but not moved from its starting offset.
5808///
5809/// TODO: Do we need to format *before* mutation in case formatting moves the
5810/// sketch block forward?
5811fn find_sketch_block_added_item(
5812    ast: &ast::Node<ast::Program>,
5813    sketch_block_before_mutation: &AstNodeRef,
5814) -> Result<AstNodeRef, KclError> {
5815    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5816        let find = FindSketchBlockByNodePath {
5817            target_node_path: node_path.clone(),
5818            found: Cell::new(None),
5819        };
5820        let node = crate::walk::Node::from(ast);
5821        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5822        find.found.into_inner().ok_or_else(|| {
5823            KclError::refactor(format!(
5824                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5825            ))
5826        })
5827    } else {
5828        // No NodePath. Fall back to legacy source range.
5829        let find = FindSketchBlockSourceRange {
5830            target_before_mutation: sketch_block_before_mutation.range,
5831            found: Cell::new(None),
5832        };
5833        let node = crate::walk::Node::from(ast);
5834        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5835        find.found.into_inner().ok_or_else(|| KclError::refactor(
5836            format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
5837        ))
5838    }
5839}
5840
5841fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5842    // TODO: Don't duplicate this from lib.rs Program.
5843    ast.recast_top(&Default::default(), 0)
5844}
5845
5846pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5847    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5848        inner: ast::ArrayExpression {
5849            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5850            non_code_meta: Default::default(),
5851            digest: None,
5852        },
5853        start: Default::default(),
5854        end: Default::default(),
5855        module_id: Default::default(),
5856        node_path: None,
5857        outer_attrs: Default::default(),
5858        pre_comments: Default::default(),
5859        comment_start: Default::default(),
5860    })))
5861}
5862
5863fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5864    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5865        ast::ArrayExpression {
5866            elements: vec![
5867                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5868                    point.x,
5869                )?)))),
5870                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5871                    point.y,
5872                )?)))),
5873            ],
5874            non_code_meta: Default::default(),
5875            digest: None,
5876        },
5877    ))))
5878}
5879
5880fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5881    match expr {
5882        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5883            inner: ast::Literal::from(to_source_number(*number)?),
5884            start: Default::default(),
5885            end: Default::default(),
5886            module_id: Default::default(),
5887            node_path: None,
5888            outer_attrs: Default::default(),
5889            pre_comments: Default::default(),
5890            comment_start: Default::default(),
5891        }))),
5892        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5893            inner: ast::SketchVar {
5894                initial: Some(Box::new(ast::Node {
5895                    inner: to_source_number(*number)?,
5896                    start: Default::default(),
5897                    end: Default::default(),
5898                    module_id: Default::default(),
5899                    node_path: None,
5900                    outer_attrs: Default::default(),
5901                    pre_comments: Default::default(),
5902                    comment_start: Default::default(),
5903                })),
5904                digest: None,
5905            },
5906            start: Default::default(),
5907            end: Default::default(),
5908            module_id: Default::default(),
5909            node_path: None,
5910            outer_attrs: Default::default(),
5911            pre_comments: Default::default(),
5912            comment_start: Default::default(),
5913        }))),
5914        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5915    }
5916}
5917
5918fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5919    Ok(ast::NumericLiteral {
5920        value: number.value,
5921        suffix: number.units,
5922        raw: format_number_literal(number.value, number.units, None)?,
5923        digest: None,
5924    })
5925}
5926
5927#[cfg(feature = "artifact-graph")]
5928fn sketch_var_initial_value_in_solver_units(number: &ast::Node<ast::NumericLiteral>) -> f64 {
5929    let unit = match number.suffix {
5930        NumericSuffix::Cm => UnitLength::Centimeters,
5931        NumericSuffix::M => UnitLength::Meters,
5932        NumericSuffix::Inch => UnitLength::Inches,
5933        NumericSuffix::Ft => UnitLength::Feet,
5934        NumericSuffix::Yd => UnitLength::Yards,
5935        _ => UnitLength::Millimeters,
5936    };
5937    crate::execution::types::adjust_length(unit, number.value, UnitLength::Millimeters).0
5938}
5939
5940#[cfg(feature = "artifact-graph")]
5941fn source_ref_primary_range(source: &SourceRef) -> Option<SourceRange> {
5942    match source {
5943        SourceRef::Simple { range, .. } => Some(*range),
5944        SourceRef::BackTrace { ranges } => ranges.first().map(|(range, _)| *range),
5945    }
5946}
5947
5948pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5949    ast::Expr::Name(Box::new(ast_name(name)))
5950}
5951
5952fn ast_name(name: String) -> ast::Node<ast::Name> {
5953    ast::Node {
5954        inner: ast::Name {
5955            name: ast::Node {
5956                inner: ast::Identifier { name, digest: None },
5957                start: Default::default(),
5958                end: Default::default(),
5959                module_id: Default::default(),
5960                node_path: None,
5961                outer_attrs: Default::default(),
5962                pre_comments: Default::default(),
5963                comment_start: Default::default(),
5964            },
5965            path: Vec::new(),
5966            abs_path: false,
5967            digest: None,
5968        },
5969        start: Default::default(),
5970        end: Default::default(),
5971        module_id: Default::default(),
5972        node_path: None,
5973        outer_attrs: Default::default(),
5974        pre_comments: Default::default(),
5975        comment_start: Default::default(),
5976    }
5977}
5978
5979pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5980    ast::Name {
5981        name: ast::Node {
5982            inner: ast::Identifier {
5983                name: name.to_owned(),
5984                digest: None,
5985            },
5986            start: Default::default(),
5987            end: Default::default(),
5988            module_id: Default::default(),
5989            node_path: None,
5990            outer_attrs: Default::default(),
5991            pre_comments: Default::default(),
5992            comment_start: Default::default(),
5993        },
5994        path: Default::default(),
5995        abs_path: false,
5996        digest: None,
5997    }
5998}
5999
6000// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6001
6002/// Create an AST node for coincident([expr1, expr2, ...])
6003pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6004    let elements = exprs.into_iter().collect::<Vec<_>>();
6005    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6006
6007    // Create array [expr1, expr2, ...]
6008    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6009        elements,
6010        digest: None,
6011        non_code_meta: Default::default(),
6012    })));
6013
6014    // Create coincident([...])
6015    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6016        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6017        unlabeled: Some(array_expr),
6018        arguments: Default::default(),
6019        digest: None,
6020        non_code_meta: Default::default(),
6021    })))
6022}
6023
6024/// Create an AST node for line(start = [...], end = [...])
6025pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6026    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6027        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6028        unlabeled: None,
6029        arguments: vec![
6030            ast::LabeledArg {
6031                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6032                arg: start_ast,
6033            },
6034            ast::LabeledArg {
6035                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6036                arg: end_ast,
6037            },
6038        ],
6039        digest: None,
6040        non_code_meta: Default::default(),
6041    })))
6042}
6043
6044/// Create an AST node for arc(start = [...], end = [...], center = [...])
6045pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6046    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6047        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6048        unlabeled: None,
6049        arguments: vec![
6050            ast::LabeledArg {
6051                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6052                arg: start_ast,
6053            },
6054            ast::LabeledArg {
6055                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6056                arg: end_ast,
6057            },
6058            ast::LabeledArg {
6059                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6060                arg: center_ast,
6061            },
6062        ],
6063        digest: None,
6064        non_code_meta: Default::default(),
6065    })))
6066}
6067
6068/// Create an AST node for circle(start = [...], center = [...])
6069pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6070    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6071        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6072        unlabeled: None,
6073        arguments: vec![
6074            ast::LabeledArg {
6075                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6076                arg: start_ast,
6077            },
6078            ast::LabeledArg {
6079                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6080                arg: center_ast,
6081            },
6082        ],
6083        digest: None,
6084        non_code_meta: Default::default(),
6085    })))
6086}
6087
6088/// Create an AST node for horizontal(line)
6089pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6090    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6091        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6092        unlabeled: Some(line_expr),
6093        arguments: Default::default(),
6094        digest: None,
6095        non_code_meta: Default::default(),
6096    })))
6097}
6098
6099/// Create an AST node for vertical(line)
6100pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6101    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6102        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6103        unlabeled: Some(line_expr),
6104        arguments: Default::default(),
6105        digest: None,
6106        non_code_meta: Default::default(),
6107    })))
6108}
6109
6110/// Create a member expression like object.property (e.g., line1.end)
6111pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6112    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6113        object: object_expr,
6114        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6115            name: ast::Node::no_src(ast::Identifier {
6116                name: property.to_string(),
6117                digest: None,
6118            }),
6119            path: Vec::new(),
6120            abs_path: false,
6121            digest: None,
6122        }))),
6123        computed: false,
6124        digest: None,
6125    })))
6126}
6127
6128/// Create an AST node for `fixed([point, [x, y]])`.
6129fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6130    // Create [x, y] array literal.
6131    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6132        position.x,
6133    )?))));
6134    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6135        position.y,
6136    )?))));
6137    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6138        elements: vec![x_literal, y_literal],
6139        digest: None,
6140        non_code_meta: Default::default(),
6141    })));
6142
6143    // Create [point, [x, y]] outer array.
6144    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6145        elements: vec![point_expr, point_array],
6146        digest: None,
6147        non_code_meta: Default::default(),
6148    })));
6149
6150    // Create fixed([...])
6151    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6152        ast::CallExpressionKw {
6153            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6154            unlabeled: Some(array_expr),
6155            arguments: Default::default(),
6156            digest: None,
6157            non_code_meta: Default::default(),
6158        },
6159    ))))
6160}
6161
6162/// Create an AST node for equalLength([line1, line2, ...])
6163pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6164    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6165        elements: line_exprs,
6166        digest: None,
6167        non_code_meta: Default::default(),
6168    })));
6169
6170    // Create equalLength([...])
6171    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6172        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6173        unlabeled: Some(array_expr),
6174        arguments: Default::default(),
6175        digest: None,
6176        non_code_meta: Default::default(),
6177    })))
6178}
6179
6180/// Create an AST node for equalRadius([seg1, seg2, ...])
6181pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6182    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6183        elements: segment_exprs,
6184        digest: None,
6185        non_code_meta: Default::default(),
6186    })));
6187
6188    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6189        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6190        unlabeled: Some(array_expr),
6191        arguments: Default::default(),
6192        digest: None,
6193        non_code_meta: Default::default(),
6194    })))
6195}
6196
6197/// Create an AST node for tangent([seg1, seg2])
6198pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6199    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6200        elements: vec![seg1_expr, seg2_expr],
6201        digest: None,
6202        non_code_meta: Default::default(),
6203    })));
6204
6205    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6206        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6207        unlabeled: Some(array_expr),
6208        arguments: Default::default(),
6209        digest: None,
6210        non_code_meta: Default::default(),
6211    })))
6212}
6213
6214/// Create an AST node for symmetric([input1, input2], axis = line)
6215pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6216    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6217        elements: input_exprs,
6218        digest: None,
6219        non_code_meta: Default::default(),
6220    })));
6221    let arguments = vec![ast::LabeledArg {
6222        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6223        arg: axis_expr,
6224    }];
6225
6226    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6227        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6228        unlabeled: Some(array_expr),
6229        arguments,
6230        digest: None,
6231        non_code_meta: Default::default(),
6232    })))
6233}
6234
6235/// Create an AST node for midpoint(segment, point = point)
6236pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6237    let arguments = vec![ast::LabeledArg {
6238        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6239        arg: point_expr,
6240    }];
6241
6242    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6243        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6244        unlabeled: Some(segment_expr),
6245        arguments,
6246        digest: None,
6247        non_code_meta: Default::default(),
6248    })))
6249}
6250
6251#[cfg(test)]
6252mod tests {
6253    use super::*;
6254    use crate::engine::PlaneName;
6255    use crate::execution::cache::SketchModeState;
6256    use crate::execution::cache::clear_mem_cache;
6257    use crate::execution::cache::read_old_memory;
6258    use crate::execution::cache::write_old_memory;
6259    use crate::front::Distance;
6260    use crate::front::Fixed;
6261    use crate::front::FixedPoint;
6262    use crate::front::Midpoint;
6263    use crate::front::Object;
6264    use crate::front::Plane;
6265    use crate::front::Sketch;
6266    use crate::front::Tangent;
6267    use crate::frontend::sketch::Vertical;
6268    use crate::pretty::NumericSuffix;
6269
6270    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6271        for object in &scene_graph.objects {
6272            if let ObjectKind::Sketch(_) = &object.kind {
6273                return Some(object);
6274            }
6275        }
6276        None
6277    }
6278
6279    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6280        for object in &scene_graph.objects {
6281            if let ObjectKind::Face(_) = &object.kind {
6282                return Some(object);
6283            }
6284        }
6285        None
6286    }
6287
6288    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6289        for object in &scene_graph.objects {
6290            if matches!(&object.kind, ObjectKind::Wall(_)) {
6291                return Some(object.id);
6292            }
6293        }
6294        None
6295    }
6296
6297    #[test]
6298    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6299        let source = "\
6300region001 = region(point = [0.1, 0.1], sketch = s)
6301extrude001 = extrude(region001, length = 5)
6302revolve001 = revolve(region001, axis = Y)
6303sweep001 = sweep(region001, path = path001)
6304loft001 = loft(region001)
6305not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6306";
6307
6308        let program = Program::parse(source).unwrap().0.unwrap();
6309
6310        assert_eq!(
6311            region_name_from_sweep_variable(&program.ast, "extrude001"),
6312            Some("region001".to_owned())
6313        );
6314        assert_eq!(
6315            region_name_from_sweep_variable(&program.ast, "revolve001"),
6316            Some("region001".to_owned())
6317        );
6318        assert_eq!(
6319            region_name_from_sweep_variable(&program.ast, "sweep001"),
6320            Some("region001".to_owned())
6321        );
6322        assert_eq!(
6323            region_name_from_sweep_variable(&program.ast, "loft001"),
6324            Some("region001".to_owned())
6325        );
6326        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6327    }
6328
6329    #[track_caller]
6330    fn expect_sketch(object: &Object) -> &Sketch {
6331        if let ObjectKind::Sketch(sketch) = &object.kind {
6332            sketch
6333        } else {
6334            panic!("Object is not a sketch: {:?}", object);
6335        }
6336    }
6337
6338    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6339        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6340        let ObjectKind::Segment {
6341            segment: Segment::Point(point),
6342        } = &point_object.kind
6343        else {
6344            panic!("Object is not a point segment: {point_object:?}");
6345        };
6346        point.position.clone()
6347    }
6348
6349    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6350        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6351        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6352    }
6353
6354    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6355        LineCtor {
6356            start: Point2d {
6357                x: Expr::Number(Number { value: start_x, units }),
6358                y: Expr::Number(Number { value: start_y, units }),
6359            },
6360            end: Point2d {
6361                x: Expr::Number(Number { value: end_x, units }),
6362                y: Expr::Number(Number { value: end_y, units }),
6363            },
6364            construction: None,
6365        }
6366    }
6367
6368    async fn create_sketch_with_single_line(
6369        frontend: &mut FrontendState,
6370        ctx: &ExecutorContext,
6371        mock_ctx: &ExecutorContext,
6372        version: Version,
6373    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6374        frontend.program = Program::empty();
6375
6376        let sketch_args = SketchCtor {
6377            on: Plane::Default(PlaneName::Xy),
6378        };
6379        let (_src_delta, _scene_delta, sketch_id) = frontend
6380            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6381            .await
6382            .unwrap();
6383
6384        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6385        let (source_delta, scene_graph_delta) = frontend
6386            .add_segment(mock_ctx, version, sketch_id, segment, None)
6387            .await
6388            .unwrap();
6389        let line_id = *scene_graph_delta
6390            .new_objects
6391            .last()
6392            .expect("Expected line object id to be created");
6393
6394        (sketch_id, line_id, source_delta, scene_graph_delta)
6395    }
6396
6397    #[tokio::test(flavor = "multi_thread")]
6398    async fn test_sketch_checkpoint_round_trip_restores_state() {
6399        let mut frontend = FrontendState::new();
6400        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6401        let mock_ctx = ExecutorContext::new_mock(None).await;
6402        let version = Version(0);
6403
6404        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6405            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6406
6407        let expected_source = source_delta.text.clone();
6408        let expected_scene_graph = frontend.scene_graph.clone();
6409        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6410        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6411
6412        let checkpoint_id = frontend
6413            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6414            .await
6415            .unwrap();
6416
6417        let edited_segments = vec![ExistingSegmentCtor {
6418            id: line_id,
6419            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6420        }];
6421        let (edited_source, _edited_scene) = frontend
6422            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6423            .await
6424            .unwrap();
6425        assert_ne!(edited_source.text, expected_source);
6426
6427        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6428
6429        assert_eq!(restored.source_delta.text, expected_source);
6430        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6431        assert!(restored.scene_graph_delta.invalidates_ids);
6432        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6433        assert_eq!(frontend.scene_graph, expected_scene_graph);
6434        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6435
6436        ctx.close().await;
6437        mock_ctx.close().await;
6438    }
6439
6440    #[tokio::test(flavor = "multi_thread")]
6441    async fn test_sketch_checkpoints_prune_oldest_entries() {
6442        let mut frontend = FrontendState::new();
6443        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6444        let mock_ctx = ExecutorContext::new_mock(None).await;
6445        let version = Version(0);
6446
6447        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6448            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6449
6450        let mut checkpoint_ids = Vec::new();
6451        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6452            checkpoint_ids.push(
6453                frontend
6454                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6455                    .await
6456                    .unwrap(),
6457            );
6458        }
6459
6460        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6461        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6462
6463        let oldest_retained = checkpoint_ids[3];
6464        assert_eq!(
6465            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6466            Some(oldest_retained)
6467        );
6468
6469        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6470        assert!(evicted_restore.is_err());
6471        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6472
6473        frontend
6474            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6475            .await
6476            .unwrap();
6477
6478        ctx.close().await;
6479        mock_ctx.close().await;
6480    }
6481
6482    #[tokio::test(flavor = "multi_thread")]
6483    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6484        let mut frontend = FrontendState::new();
6485        let missing_checkpoint = SketchCheckpointId::new(999);
6486
6487        let err = frontend
6488            .restore_sketch_checkpoint(missing_checkpoint)
6489            .await
6490            .expect_err("Expected restore to fail for missing checkpoint");
6491
6492        assert!(err.msg.contains("Sketch checkpoint not found"));
6493    }
6494
6495    #[tokio::test(flavor = "multi_thread")]
6496    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6497        let mut frontend = FrontendState::new();
6498        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6499        let mock_ctx = ExecutorContext::new_mock(None).await;
6500        let version = Version(0);
6501
6502        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6503            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6504
6505        let checkpoint_a = frontend
6506            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6507            .await
6508            .unwrap();
6509        let checkpoint_b = frontend
6510            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6511            .await
6512            .unwrap();
6513        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6514
6515        frontend.clear_sketch_checkpoints();
6516        assert!(frontend.sketch_checkpoints.is_empty());
6517        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6518        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6519
6520        ctx.close().await;
6521        mock_ctx.close().await;
6522    }
6523
6524    #[tokio::test(flavor = "multi_thread")]
6525    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6526        let mut frontend = FrontendState::new();
6527        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6528        let mock_ctx = ExecutorContext::new_mock(None).await;
6529        let version = Version(0);
6530
6531        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6532            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6533        let old_source = source_delta.text.clone();
6534        let old_checkpoint = frontend
6535            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6536            .await
6537            .unwrap();
6538        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6539
6540        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6541            .unwrap()
6542            .0
6543            .unwrap();
6544
6545        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6546        let SetProgramOutcome::Success {
6547            checkpoint_id: Some(new_checkpoint),
6548            ..
6549        } = result
6550        else {
6551            panic!("Expected Success with a fresh checkpoint baseline");
6552        };
6553
6554        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6555
6556        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6557        assert_eq!(old_restore.source_delta.text, old_source);
6558
6559        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6560        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6561
6562        ctx.close().await;
6563        mock_ctx.close().await;
6564    }
6565
6566    #[tokio::test(flavor = "multi_thread")]
6567    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6568        let mut frontend = FrontendState::new();
6569        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6570        let mock_ctx = ExecutorContext::new_mock(None).await;
6571        let version = Version(0);
6572
6573        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6574            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6575        let old_checkpoint = frontend
6576            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6577            .await
6578            .unwrap();
6579        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6580
6581        let failing_program = Program::parse(
6582            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6583        )
6584        .unwrap()
6585        .0
6586        .unwrap();
6587
6588        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6589        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6590        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6591        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6592
6593        ctx.close().await;
6594        mock_ctx.close().await;
6595    }
6596
6597    #[tokio::test(flavor = "multi_thread")]
6598    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6599        let mut frontend = FrontendState::new();
6600        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6601
6602        let program = Program::parse(
6603            "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n  distance([line1.start, line1.end]) == width\n}\n",
6604        )
6605        .unwrap()
6606        .0
6607        .unwrap();
6608        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6609        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6610            panic!("Expected successful baseline program execution");
6611        };
6612
6613        clear_mem_cache().await;
6614        assert!(read_old_memory().await.is_none());
6615
6616        let checkpoint_without_mock_memory = frontend
6617            .create_sketch_checkpoint((*exec_outcome).clone())
6618            .await
6619            .unwrap();
6620
6621        write_old_memory(SketchModeState::new_for_tests()).await;
6622        assert!(read_old_memory().await.is_some());
6623
6624        let checkpoint_with_mock_memory = frontend
6625            .create_sketch_checkpoint((*exec_outcome).clone())
6626            .await
6627            .unwrap();
6628
6629        clear_mem_cache().await;
6630        assert!(read_old_memory().await.is_none());
6631
6632        frontend
6633            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6634            .await
6635            .unwrap();
6636        assert!(read_old_memory().await.is_some());
6637
6638        frontend
6639            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6640            .await
6641            .unwrap();
6642        assert!(read_old_memory().await.is_none());
6643
6644        ctx.close().await;
6645    }
6646
6647    #[tokio::test(flavor = "multi_thread")]
6648    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6649        let source = "\
6650sketch(on = XY) {
6651  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6652}
6653
6654bad = missing_name
6655";
6656        let program = Program::parse(source).unwrap().0.unwrap();
6657
6658        let mut frontend = FrontendState::new();
6659
6660        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6661        let mock_ctx = ExecutorContext::new_mock(None).await;
6662        let version = Version(0);
6663        let project_id = ProjectId(0);
6664        let file_id = FileId(0);
6665
6666        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6667            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6668        };
6669
6670        let sketch_id = frontend
6671            .scene_graph
6672            .objects
6673            .iter()
6674            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6675            .expect("Expected sketch object from errored hack_set_program");
6676
6677        frontend
6678            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6679            .await
6680            .unwrap();
6681
6682        ctx.close().await;
6683        mock_ctx.close().await;
6684    }
6685
6686    #[tokio::test(flavor = "multi_thread")]
6687    async fn test_new_sketch_add_point_edit_point() {
6688        let program = Program::empty();
6689
6690        let mut frontend = FrontendState::new();
6691        frontend.program = program;
6692
6693        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6694        let mock_ctx = ExecutorContext::new_mock(None).await;
6695        let version = Version(0);
6696
6697        let sketch_args = SketchCtor {
6698            on: Plane::Default(PlaneName::Xy),
6699        };
6700        let (_src_delta, scene_delta, sketch_id) = frontend
6701            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6702            .await
6703            .unwrap();
6704        assert_eq!(sketch_id, ObjectId(1));
6705        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6706        let sketch_object = &scene_delta.new_graph.objects[1];
6707        assert_eq!(sketch_object.id, ObjectId(1));
6708        assert_eq!(
6709            sketch_object.kind,
6710            ObjectKind::Sketch(Sketch {
6711                args: SketchCtor {
6712                    on: Plane::Default(PlaneName::Xy)
6713                },
6714                plane: ObjectId(0),
6715                segments: vec![],
6716                constraints: vec![],
6717            })
6718        );
6719        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6720
6721        let point_ctor = PointCtor {
6722            position: Point2d {
6723                x: Expr::Number(Number {
6724                    value: 1.0,
6725                    units: NumericSuffix::Inch,
6726                }),
6727                y: Expr::Number(Number {
6728                    value: 2.0,
6729                    units: NumericSuffix::Inch,
6730                }),
6731            },
6732        };
6733        let segment = SegmentCtor::Point(point_ctor);
6734        let (src_delta, scene_delta) = frontend
6735            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6736            .await
6737            .unwrap();
6738        assert_eq!(
6739            src_delta.text.as_str(),
6740            "sketch001 = sketch(on = XY) {
6741  point(at = [1in, 2in])
6742}
6743"
6744        );
6745        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6746        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6747        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6748            assert_eq!(scene_object.id.0, i);
6749        }
6750
6751        let point_id = *scene_delta.new_objects.last().unwrap();
6752
6753        let point_ctor = PointCtor {
6754            position: Point2d {
6755                x: Expr::Number(Number {
6756                    value: 3.0,
6757                    units: NumericSuffix::Inch,
6758                }),
6759                y: Expr::Number(Number {
6760                    value: 4.0,
6761                    units: NumericSuffix::Inch,
6762                }),
6763            },
6764        };
6765        let segments = vec![ExistingSegmentCtor {
6766            id: point_id,
6767            ctor: SegmentCtor::Point(point_ctor),
6768        }];
6769        let (src_delta, scene_delta) = frontend
6770            .edit_segments(&mock_ctx, version, sketch_id, segments)
6771            .await
6772            .unwrap();
6773        assert_eq!(
6774            src_delta.text.as_str(),
6775            "sketch001 = sketch(on = XY) {
6776  point(at = [3in, 4in])
6777}
6778"
6779        );
6780        assert_eq!(scene_delta.new_objects, vec![]);
6781        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6782
6783        ctx.close().await;
6784        mock_ctx.close().await;
6785    }
6786
6787    #[tokio::test(flavor = "multi_thread")]
6788    async fn test_new_sketch_add_line_edit_line() {
6789        let program = Program::empty();
6790
6791        let mut frontend = FrontendState::new();
6792        frontend.program = program;
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        let sketch_args = SketchCtor {
6799            on: Plane::Default(PlaneName::Xy),
6800        };
6801        let (_src_delta, scene_delta, sketch_id) = frontend
6802            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6803            .await
6804            .unwrap();
6805        assert_eq!(sketch_id, ObjectId(1));
6806        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6807        let sketch_object = &scene_delta.new_graph.objects[1];
6808        assert_eq!(sketch_object.id, ObjectId(1));
6809        assert_eq!(
6810            sketch_object.kind,
6811            ObjectKind::Sketch(Sketch {
6812                args: SketchCtor {
6813                    on: Plane::Default(PlaneName::Xy)
6814                },
6815                plane: ObjectId(0),
6816                segments: vec![],
6817                constraints: vec![],
6818            })
6819        );
6820        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6821
6822        let line_ctor = LineCtor {
6823            start: Point2d {
6824                x: Expr::Number(Number {
6825                    value: 0.0,
6826                    units: NumericSuffix::Mm,
6827                }),
6828                y: Expr::Number(Number {
6829                    value: 0.0,
6830                    units: NumericSuffix::Mm,
6831                }),
6832            },
6833            end: Point2d {
6834                x: Expr::Number(Number {
6835                    value: 10.0,
6836                    units: NumericSuffix::Mm,
6837                }),
6838                y: Expr::Number(Number {
6839                    value: 10.0,
6840                    units: NumericSuffix::Mm,
6841                }),
6842            },
6843            construction: None,
6844        };
6845        let segment = SegmentCtor::Line(line_ctor);
6846        let (src_delta, scene_delta) = frontend
6847            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6848            .await
6849            .unwrap();
6850        assert_eq!(
6851            src_delta.text.as_str(),
6852            "sketch001 = sketch(on = XY) {
6853  line(start = [0mm, 0mm], end = [10mm, 10mm])
6854}
6855"
6856        );
6857        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6858        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6859        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6860            assert_eq!(scene_object.id.0, i);
6861        }
6862
6863        // The new objects are the end points and then the line.
6864        let line = *scene_delta.new_objects.last().unwrap();
6865
6866        let line_ctor = LineCtor {
6867            start: Point2d {
6868                x: Expr::Number(Number {
6869                    value: 1.0,
6870                    units: NumericSuffix::Mm,
6871                }),
6872                y: Expr::Number(Number {
6873                    value: 2.0,
6874                    units: NumericSuffix::Mm,
6875                }),
6876            },
6877            end: Point2d {
6878                x: Expr::Number(Number {
6879                    value: 13.0,
6880                    units: NumericSuffix::Mm,
6881                }),
6882                y: Expr::Number(Number {
6883                    value: 14.0,
6884                    units: NumericSuffix::Mm,
6885                }),
6886            },
6887            construction: None,
6888        };
6889        let segments = vec![ExistingSegmentCtor {
6890            id: line,
6891            ctor: SegmentCtor::Line(line_ctor),
6892        }];
6893        let (src_delta, scene_delta) = frontend
6894            .edit_segments(&mock_ctx, version, sketch_id, segments)
6895            .await
6896            .unwrap();
6897        assert_eq!(
6898            src_delta.text.as_str(),
6899            "sketch001 = sketch(on = XY) {
6900  line(start = [1mm, 2mm], end = [13mm, 14mm])
6901}
6902"
6903        );
6904        assert_eq!(scene_delta.new_objects, vec![]);
6905        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6906
6907        ctx.close().await;
6908        mock_ctx.close().await;
6909    }
6910
6911    #[tokio::test(flavor = "multi_thread")]
6912    async fn test_new_sketch_add_arc_edit_arc() {
6913        let program = Program::empty();
6914
6915        let mut frontend = FrontendState::new();
6916        frontend.program = program;
6917
6918        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6919        let mock_ctx = ExecutorContext::new_mock(None).await;
6920        let version = Version(0);
6921
6922        let sketch_args = SketchCtor {
6923            on: Plane::Default(PlaneName::Xy),
6924        };
6925        let (_src_delta, scene_delta, sketch_id) = frontend
6926            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6927            .await
6928            .unwrap();
6929        assert_eq!(sketch_id, ObjectId(1));
6930        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6931        let sketch_object = &scene_delta.new_graph.objects[1];
6932        assert_eq!(sketch_object.id, ObjectId(1));
6933        assert_eq!(
6934            sketch_object.kind,
6935            ObjectKind::Sketch(Sketch {
6936                args: SketchCtor {
6937                    on: Plane::Default(PlaneName::Xy),
6938                },
6939                plane: ObjectId(0),
6940                segments: vec![],
6941                constraints: vec![],
6942            })
6943        );
6944        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6945
6946        let arc_ctor = ArcCtor {
6947            start: Point2d {
6948                x: Expr::Var(Number {
6949                    value: 0.0,
6950                    units: NumericSuffix::Mm,
6951                }),
6952                y: Expr::Var(Number {
6953                    value: 0.0,
6954                    units: NumericSuffix::Mm,
6955                }),
6956            },
6957            end: Point2d {
6958                x: Expr::Var(Number {
6959                    value: 10.0,
6960                    units: NumericSuffix::Mm,
6961                }),
6962                y: Expr::Var(Number {
6963                    value: 10.0,
6964                    units: NumericSuffix::Mm,
6965                }),
6966            },
6967            center: Point2d {
6968                x: Expr::Var(Number {
6969                    value: 10.0,
6970                    units: NumericSuffix::Mm,
6971                }),
6972                y: Expr::Var(Number {
6973                    value: 0.0,
6974                    units: NumericSuffix::Mm,
6975                }),
6976            },
6977            construction: None,
6978        };
6979        let segment = SegmentCtor::Arc(arc_ctor);
6980        let (src_delta, scene_delta) = frontend
6981            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6982            .await
6983            .unwrap();
6984        assert_eq!(
6985            src_delta.text.as_str(),
6986            "sketch001 = sketch(on = XY) {
6987  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6988}
6989"
6990        );
6991        assert_eq!(
6992            scene_delta.new_objects,
6993            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6994        );
6995        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6996            assert_eq!(scene_object.id.0, i);
6997        }
6998        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6999
7000        // The new objects are the end points, the center, and then the arc.
7001        let arc = *scene_delta.new_objects.last().unwrap();
7002
7003        let arc_ctor = ArcCtor {
7004            start: Point2d {
7005                x: Expr::Var(Number {
7006                    value: 1.0,
7007                    units: NumericSuffix::Mm,
7008                }),
7009                y: Expr::Var(Number {
7010                    value: 2.0,
7011                    units: NumericSuffix::Mm,
7012                }),
7013            },
7014            end: Point2d {
7015                x: Expr::Var(Number {
7016                    value: 13.0,
7017                    units: NumericSuffix::Mm,
7018                }),
7019                y: Expr::Var(Number {
7020                    value: 14.0,
7021                    units: NumericSuffix::Mm,
7022                }),
7023            },
7024            center: Point2d {
7025                x: Expr::Var(Number {
7026                    value: 13.0,
7027                    units: NumericSuffix::Mm,
7028                }),
7029                y: Expr::Var(Number {
7030                    value: 2.0,
7031                    units: NumericSuffix::Mm,
7032                }),
7033            },
7034            construction: None,
7035        };
7036        let segments = vec![ExistingSegmentCtor {
7037            id: arc,
7038            ctor: SegmentCtor::Arc(arc_ctor),
7039        }];
7040        let (src_delta, scene_delta) = frontend
7041            .edit_segments(&mock_ctx, version, sketch_id, segments)
7042            .await
7043            .unwrap();
7044        assert_eq!(
7045            src_delta.text.as_str(),
7046            "sketch001 = sketch(on = XY) {
7047  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7048}
7049"
7050        );
7051        assert_eq!(scene_delta.new_objects, vec![]);
7052        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7053
7054        ctx.close().await;
7055        mock_ctx.close().await;
7056    }
7057
7058    #[tokio::test(flavor = "multi_thread")]
7059    async fn test_new_sketch_add_circle_edit_circle() {
7060        let program = Program::empty();
7061
7062        let mut frontend = FrontendState::new();
7063        frontend.program = program;
7064
7065        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7066        let mock_ctx = ExecutorContext::new_mock(None).await;
7067        let version = Version(0);
7068
7069        let sketch_args = SketchCtor {
7070            on: Plane::Default(PlaneName::Xy),
7071        };
7072        let (_src_delta, _scene_delta, sketch_id) = frontend
7073            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7074            .await
7075            .unwrap();
7076
7077        // Add a circle segment.
7078        let circle_ctor = CircleCtor {
7079            start: Point2d {
7080                x: Expr::Var(Number {
7081                    value: 5.0,
7082                    units: NumericSuffix::Mm,
7083                }),
7084                y: Expr::Var(Number {
7085                    value: 0.0,
7086                    units: NumericSuffix::Mm,
7087                }),
7088            },
7089            center: Point2d {
7090                x: Expr::Var(Number {
7091                    value: 0.0,
7092                    units: NumericSuffix::Mm,
7093                }),
7094                y: Expr::Var(Number {
7095                    value: 0.0,
7096                    units: NumericSuffix::Mm,
7097                }),
7098            },
7099            construction: None,
7100        };
7101        let segment = SegmentCtor::Circle(circle_ctor);
7102        let (src_delta, scene_delta) = frontend
7103            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7104            .await
7105            .unwrap();
7106        assert_eq!(
7107            src_delta.text.as_str(),
7108            "sketch001 = sketch(on = XY) {
7109  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7110}
7111"
7112        );
7113        // The new objects are start, center, and then the circle segment.
7114        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7115        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7116
7117        let circle = *scene_delta.new_objects.last().unwrap();
7118
7119        // Edit the circle segment.
7120        let circle_ctor = CircleCtor {
7121            start: Point2d {
7122                x: Expr::Var(Number {
7123                    value: 10.0,
7124                    units: NumericSuffix::Mm,
7125                }),
7126                y: Expr::Var(Number {
7127                    value: 0.0,
7128                    units: NumericSuffix::Mm,
7129                }),
7130            },
7131            center: Point2d {
7132                x: Expr::Var(Number {
7133                    value: 3.0,
7134                    units: NumericSuffix::Mm,
7135                }),
7136                y: Expr::Var(Number {
7137                    value: 4.0,
7138                    units: NumericSuffix::Mm,
7139                }),
7140            },
7141            construction: None,
7142        };
7143        let segments = vec![ExistingSegmentCtor {
7144            id: circle,
7145            ctor: SegmentCtor::Circle(circle_ctor),
7146        }];
7147        let (src_delta, scene_delta) = frontend
7148            .edit_segments(&mock_ctx, version, sketch_id, segments)
7149            .await
7150            .unwrap();
7151        assert_eq!(
7152            src_delta.text.as_str(),
7153            "sketch001 = sketch(on = XY) {
7154  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7155}
7156"
7157        );
7158        assert_eq!(scene_delta.new_objects, vec![]);
7159        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7160
7161        ctx.close().await;
7162        mock_ctx.close().await;
7163    }
7164
7165    #[tokio::test(flavor = "multi_thread")]
7166    async fn test_delete_circle() {
7167        let initial_source = "sketch001 = sketch(on = XY) {
7168  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7169}
7170";
7171
7172        let program = Program::parse(initial_source).unwrap().0.unwrap();
7173        let mut frontend = FrontendState::new();
7174
7175        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7176        let mock_ctx = ExecutorContext::new_mock(None).await;
7177        let version = Version(0);
7178
7179        frontend.hack_set_program(&ctx, program).await.unwrap();
7180        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7181        let sketch_id = sketch_object.id;
7182        let sketch = expect_sketch(sketch_object);
7183
7184        // The sketch should have 3 segments: start point, center point, and the circle.
7185        assert_eq!(sketch.segments.len(), 3);
7186        let circle_id = sketch.segments[2];
7187
7188        // Delete the circle.
7189        let (src_delta, scene_delta) = frontend
7190            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7191            .await
7192            .unwrap();
7193        assert_eq!(
7194            src_delta.text.as_str(),
7195            "sketch001 = sketch(on = XY) {
7196}
7197"
7198        );
7199        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7200        let new_sketch = expect_sketch(new_sketch_object);
7201        assert_eq!(new_sketch.segments.len(), 0);
7202
7203        ctx.close().await;
7204        mock_ctx.close().await;
7205    }
7206
7207    #[tokio::test(flavor = "multi_thread")]
7208    async fn test_edit_circle_via_point() {
7209        let initial_source = "sketch001 = sketch(on = XY) {
7210  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7211}
7212";
7213
7214        let program = Program::parse(initial_source).unwrap().0.unwrap();
7215        let mut frontend = FrontendState::new();
7216
7217        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7218        let mock_ctx = ExecutorContext::new_mock(None).await;
7219        let version = Version(0);
7220
7221        frontend.hack_set_program(&ctx, program).await.unwrap();
7222        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7223        let sketch_id = sketch_object.id;
7224        let sketch = expect_sketch(sketch_object);
7225
7226        // Find the circle segment and its start point.
7227        let circle_id = sketch
7228            .segments
7229            .iter()
7230            .copied()
7231            .find(|seg_id| {
7232                matches!(
7233                    &frontend.scene_graph.objects[seg_id.0].kind,
7234                    ObjectKind::Segment {
7235                        segment: Segment::Circle(_)
7236                    }
7237                )
7238            })
7239            .expect("Expected a circle segment in sketch");
7240        let circle_object = &frontend.scene_graph.objects[circle_id.0];
7241        let ObjectKind::Segment {
7242            segment: Segment::Circle(circle),
7243        } = &circle_object.kind
7244        else {
7245            panic!("Expected circle segment, got: {:?}", circle_object.kind);
7246        };
7247        let start_point_id = circle.start;
7248
7249        // Edit the start point via SegmentCtor::Point.
7250        let segments = vec![ExistingSegmentCtor {
7251            id: start_point_id,
7252            ctor: SegmentCtor::Point(PointCtor {
7253                position: Point2d {
7254                    x: Expr::Var(Number {
7255                        value: 7.0,
7256                        units: NumericSuffix::Mm,
7257                    }),
7258                    y: Expr::Var(Number {
7259                        value: 1.0,
7260                        units: NumericSuffix::Mm,
7261                    }),
7262                },
7263            }),
7264        }];
7265        let (src_delta, _scene_delta) = frontend
7266            .edit_segments(&mock_ctx, version, sketch_id, segments)
7267            .await
7268            .unwrap();
7269        assert_eq!(
7270            src_delta.text.as_str(),
7271            "sketch001 = sketch(on = XY) {
7272  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7273}
7274"
7275        );
7276
7277        ctx.close().await;
7278        mock_ctx.close().await;
7279    }
7280
7281    #[tokio::test(flavor = "multi_thread")]
7282    async fn test_add_line_when_sketch_block_uses_variable() {
7283        let initial_source = "s = sketch(on = XY) {}
7284";
7285
7286        let program = Program::parse(initial_source).unwrap().0.unwrap();
7287
7288        let mut frontend = FrontendState::new();
7289
7290        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7291        let mock_ctx = ExecutorContext::new_mock(None).await;
7292        let version = Version(0);
7293
7294        frontend.hack_set_program(&ctx, program).await.unwrap();
7295        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7296        let sketch_id = sketch_object.id;
7297
7298        let line_ctor = LineCtor {
7299            start: Point2d {
7300                x: Expr::Number(Number {
7301                    value: 0.0,
7302                    units: NumericSuffix::Mm,
7303                }),
7304                y: Expr::Number(Number {
7305                    value: 0.0,
7306                    units: NumericSuffix::Mm,
7307                }),
7308            },
7309            end: Point2d {
7310                x: Expr::Number(Number {
7311                    value: 10.0,
7312                    units: NumericSuffix::Mm,
7313                }),
7314                y: Expr::Number(Number {
7315                    value: 10.0,
7316                    units: NumericSuffix::Mm,
7317                }),
7318            },
7319            construction: None,
7320        };
7321        let segment = SegmentCtor::Line(line_ctor);
7322        let (src_delta, scene_delta) = frontend
7323            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7324            .await
7325            .unwrap();
7326        assert_eq!(
7327            src_delta.text.as_str(),
7328            "s = sketch(on = XY) {
7329  line(start = [0mm, 0mm], end = [10mm, 10mm])
7330}
7331"
7332        );
7333        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7334        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7335
7336        ctx.close().await;
7337        mock_ctx.close().await;
7338    }
7339
7340    #[tokio::test(flavor = "multi_thread")]
7341    async fn test_new_sketch_add_line_delete_sketch() {
7342        let program = Program::empty();
7343
7344        let mut frontend = FrontendState::new();
7345        frontend.program = program;
7346
7347        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7348        let mock_ctx = ExecutorContext::new_mock(None).await;
7349        let version = Version(0);
7350
7351        let sketch_args = SketchCtor {
7352            on: Plane::Default(PlaneName::Xy),
7353        };
7354        let (_src_delta, scene_delta, sketch_id) = frontend
7355            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7356            .await
7357            .unwrap();
7358        assert_eq!(sketch_id, ObjectId(1));
7359        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7360        let sketch_object = &scene_delta.new_graph.objects[1];
7361        assert_eq!(sketch_object.id, ObjectId(1));
7362        assert_eq!(
7363            sketch_object.kind,
7364            ObjectKind::Sketch(Sketch {
7365                args: SketchCtor {
7366                    on: Plane::Default(PlaneName::Xy)
7367                },
7368                plane: ObjectId(0),
7369                segments: vec![],
7370                constraints: vec![],
7371            })
7372        );
7373        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7374
7375        let line_ctor = LineCtor {
7376            start: Point2d {
7377                x: Expr::Number(Number {
7378                    value: 0.0,
7379                    units: NumericSuffix::Mm,
7380                }),
7381                y: Expr::Number(Number {
7382                    value: 0.0,
7383                    units: NumericSuffix::Mm,
7384                }),
7385            },
7386            end: Point2d {
7387                x: Expr::Number(Number {
7388                    value: 10.0,
7389                    units: NumericSuffix::Mm,
7390                }),
7391                y: Expr::Number(Number {
7392                    value: 10.0,
7393                    units: NumericSuffix::Mm,
7394                }),
7395            },
7396            construction: None,
7397        };
7398        let segment = SegmentCtor::Line(line_ctor);
7399        let (src_delta, scene_delta) = frontend
7400            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7401            .await
7402            .unwrap();
7403        assert_eq!(
7404            src_delta.text.as_str(),
7405            "sketch001 = sketch(on = XY) {
7406  line(start = [0mm, 0mm], end = [10mm, 10mm])
7407}
7408"
7409        );
7410        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7411
7412        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7413        assert_eq!(src_delta.text.as_str(), "");
7414        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7415
7416        ctx.close().await;
7417        mock_ctx.close().await;
7418    }
7419
7420    #[tokio::test(flavor = "multi_thread")]
7421    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7422        let initial_source = "s = sketch(on = XY) {}
7423";
7424
7425        let program = Program::parse(initial_source).unwrap().0.unwrap();
7426
7427        let mut frontend = FrontendState::new();
7428
7429        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7430        let mock_ctx = ExecutorContext::new_mock(None).await;
7431        let version = Version(0);
7432
7433        frontend.hack_set_program(&ctx, program).await.unwrap();
7434        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7435        let sketch_id = sketch_object.id;
7436
7437        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7438        assert_eq!(src_delta.text.as_str(), "");
7439        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7440
7441        ctx.close().await;
7442        mock_ctx.close().await;
7443    }
7444
7445    #[tokio::test(flavor = "multi_thread")]
7446    async fn test_delete_sketch_after_comment() {
7447        let initial_source = "sketch001 = sketch(on = XZ) {
7448}
7449";
7450
7451        let program = Program::parse(initial_source).unwrap().0.unwrap();
7452        let mut frontend = FrontendState::new();
7453
7454        let ctx = ExecutorContext::new_with_engine(
7455            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7456            Default::default(),
7457        );
7458        let version = Version(0);
7459
7460        frontend.hack_set_program(&ctx, program).await.unwrap();
7461        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7462        let sketch_id = sketch_object.id;
7463        let original_source = sketch_object.source.clone();
7464
7465        let commented_source = "// test 1
7466sketch001 = sketch(on = XZ) {
7467}
7468";
7469        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7470        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7471
7472        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7473        assert_eq!(cached_sketch_object.source, original_source);
7474
7475        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7476        assert!(
7477            !src_delta.text.contains("sketch001"),
7478            "sketch was not deleted: {}",
7479            src_delta.text
7480        );
7481        // The leading line comment must survive deletion.
7482        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7483        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7484
7485        ctx.close().await;
7486    }
7487
7488    #[tokio::test(flavor = "multi_thread")]
7489    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7490        let initial_source = "sketch001 = sketch(on = XZ) {
7491}
7492foo = 1
7493";
7494
7495        let program = Program::parse(initial_source).unwrap().0.unwrap();
7496        let mut frontend = FrontendState::new();
7497
7498        let ctx = ExecutorContext::new_with_engine(
7499            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7500            Default::default(),
7501        );
7502        let version = Version(0);
7503
7504        frontend.hack_set_program(&ctx, program).await.unwrap();
7505        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7506        let sketch_id = sketch_object.id;
7507
7508        let commented_source = "// keep me
7509sketch001 = sketch(on = XZ) {
7510}
7511foo = 1
7512";
7513        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7514        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7515
7516        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7517        // The leading comment should remain, now attached to the following body item.
7518        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7519
7520        ctx.close().await;
7521    }
7522
7523    #[tokio::test(flavor = "multi_thread")]
7524    async fn test_delete_segment_preserves_pre_comment() {
7525        let initial_source = "\
7526sketch(on = XY) {
7527  point(at = [var 1, var 2])
7528  // describe the middle point
7529  point(at = [var 3, var 4])
7530  point(at = [var 5, var 6])
7531}
7532";
7533
7534        let program = Program::parse(initial_source).unwrap().0.unwrap();
7535        let mut frontend = FrontendState::new();
7536
7537        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7538        let mock_ctx = ExecutorContext::new_mock(None).await;
7539        let version = Version(0);
7540
7541        frontend.hack_set_program(&ctx, program).await.unwrap();
7542        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7543        let sketch_id = sketch_object.id;
7544        let sketch = expect_sketch(sketch_object);
7545
7546        let middle_point_id = *sketch.segments.get(1).unwrap();
7547
7548        let (src_delta, _scene_delta) = frontend
7549            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7550            .await
7551            .unwrap();
7552        // The line comment on the line above the deleted point must be preserved.
7553        // It is reattached to the next surviving body item.
7554        assert_eq!(
7555            src_delta.text.as_str(),
7556            "\
7557sketch(on = XY) {
7558  point(at = [var 1mm, var 2mm])
7559  // describe the middle point
7560  point(at = [var 5mm, var 6mm])
7561}
7562"
7563        );
7564
7565        ctx.close().await;
7566        mock_ctx.close().await;
7567    }
7568
7569    #[tokio::test(flavor = "multi_thread")]
7570    async fn test_delete_last_segment_preserves_pre_comment() {
7571        let initial_source = "\
7572sketch(on = XY) {
7573  point(at = [var 1, var 2])
7574  // describe the trailing point
7575  point(at = [var 3, var 4])
7576}
7577";
7578
7579        let program = Program::parse(initial_source).unwrap().0.unwrap();
7580        let mut frontend = FrontendState::new();
7581
7582        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7583        let mock_ctx = ExecutorContext::new_mock(None).await;
7584        let version = Version(0);
7585
7586        frontend.hack_set_program(&ctx, program).await.unwrap();
7587        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7588        let sketch_id = sketch_object.id;
7589        let sketch = expect_sketch(sketch_object);
7590
7591        let last_point_id = *sketch.segments.last().unwrap();
7592
7593        let (src_delta, _scene_delta) = frontend
7594            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7595            .await
7596            .unwrap();
7597        // No following item to attach to; the comment is kept inside the sketch
7598        // block as trailing non-code metadata so the user does not lose it.
7599        assert_eq!(
7600            src_delta.text.as_str(),
7601            "\
7602sketch(on = XY) {
7603  point(at = [var 1mm, var 2mm])
7604  // describe the trailing point
7605}
7606"
7607        );
7608
7609        ctx.close().await;
7610        mock_ctx.close().await;
7611    }
7612
7613    #[tokio::test(flavor = "multi_thread")]
7614    async fn test_delete_segment_drops_inline_trailing_comment() {
7615        let initial_source = "\
7616sketch(on = XY) {
7617  point(at = [var 1, var 2])
7618  point(at = [var 3, var 4]) // same-line note that gets dropped
7619  point(at = [var 5, var 6])
7620}
7621";
7622
7623        let program = Program::parse(initial_source).unwrap().0.unwrap();
7624        let mut frontend = FrontendState::new();
7625
7626        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7627        let mock_ctx = ExecutorContext::new_mock(None).await;
7628        let version = Version(0);
7629
7630        frontend.hack_set_program(&ctx, program).await.unwrap();
7631        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7632        let sketch_id = sketch_object.id;
7633        let sketch = expect_sketch(sketch_object);
7634
7635        let middle_point_id = *sketch.segments.get(1).unwrap();
7636
7637        let (src_delta, _scene_delta) = frontend
7638            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7639            .await
7640            .unwrap();
7641        // The same-line trailing comment is removed along with the deleted code.
7642        assert!(
7643            !src_delta.text.contains("same-line note"),
7644            "inline comment should have been removed: {}",
7645            src_delta.text
7646        );
7647
7648        ctx.close().await;
7649        mock_ctx.close().await;
7650    }
7651
7652    #[tokio::test(flavor = "multi_thread")]
7653    async fn test_delete_segments_preserves_block_comments_across_positions() {
7654        // One test exercising several `delete_body_item_preserving_pre_comments`
7655        // branches at once with `/* ... */` block comments:
7656        //   - first point: leading block comment must migrate to the next item.
7657        //   - first point: same-line trailing block comment must be dropped.
7658        //   - middle point: leading block comment must stay attached after migration.
7659        //   - last point: leading block comment, with no surviving next item,
7660        //     must be converted into a trailing NonCodeNode.
7661        let initial_source = "\
7662sketch(on = XY) {
7663  /* above first - moves to middle */
7664  point(at = [var 1, var 2]) /* same-line on first - dropped */
7665  /* above middle - stays */
7666  point(at = [var 3, var 4])
7667  /* above last - moves to trailing meta */
7668  point(at = [var 5, var 6])
7669}
7670";
7671
7672        let program = Program::parse(initial_source).unwrap().0.unwrap();
7673        let mut frontend = FrontendState::new();
7674
7675        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7676        let mock_ctx = ExecutorContext::new_mock(None).await;
7677        let version = Version(0);
7678
7679        frontend.hack_set_program(&ctx, program).await.unwrap();
7680        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7681        let sketch_id = sketch_object.id;
7682        let sketch = expect_sketch(sketch_object);
7683
7684        let first_point_id = *sketch.segments.first().unwrap();
7685        let last_point_id = *sketch.segments.last().unwrap();
7686
7687        let (src_delta, _scene_delta) = frontend
7688            .delete_objects(
7689                &mock_ctx,
7690                version,
7691                sketch_id,
7692                Vec::new(),
7693                vec![first_point_id, last_point_id],
7694            )
7695            .await
7696            .unwrap();
7697        assert_eq!(
7698            src_delta.text.as_str(),
7699            "\
7700sketch(on = XY) {
7701  /* above first - moves to middle */
7702  /* above middle - stays */
7703  point(at = [var 3mm, var 4mm])
7704  /* above last - moves to trailing meta */
7705}
7706"
7707        );
7708
7709        ctx.close().await;
7710        mock_ctx.close().await;
7711    }
7712
7713    #[tokio::test(flavor = "multi_thread")]
7714    async fn test_edit_line_when_editing_its_start_point() {
7715        let initial_source = "\
7716sketch(on = XY) {
7717  line(start = [var 1, var 2], end = [var 3, var 4])
7718}
7719";
7720
7721        let program = Program::parse(initial_source).unwrap().0.unwrap();
7722
7723        let mut frontend = FrontendState::new();
7724
7725        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7726        let mock_ctx = ExecutorContext::new_mock(None).await;
7727        let version = Version(0);
7728
7729        frontend.hack_set_program(&ctx, program).await.unwrap();
7730        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7731        let sketch_id = sketch_object.id;
7732        let sketch = expect_sketch(sketch_object);
7733
7734        let point_id = *sketch.segments.first().unwrap();
7735
7736        let point_ctor = PointCtor {
7737            position: Point2d {
7738                x: Expr::Var(Number {
7739                    value: 5.0,
7740                    units: NumericSuffix::Inch,
7741                }),
7742                y: Expr::Var(Number {
7743                    value: 6.0,
7744                    units: NumericSuffix::Inch,
7745                }),
7746            },
7747        };
7748        let segments = vec![ExistingSegmentCtor {
7749            id: point_id,
7750            ctor: SegmentCtor::Point(point_ctor),
7751        }];
7752        let (src_delta, scene_delta) = frontend
7753            .edit_segments(&mock_ctx, version, sketch_id, segments)
7754            .await
7755            .unwrap();
7756        assert_eq!(
7757            src_delta.text.as_str(),
7758            "\
7759sketch(on = XY) {
7760  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7761}
7762"
7763        );
7764        assert_eq!(scene_delta.new_objects, vec![]);
7765        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7766
7767        ctx.close().await;
7768        mock_ctx.close().await;
7769    }
7770
7771    #[tokio::test(flavor = "multi_thread")]
7772    async fn test_edit_line_when_editing_its_end_point() {
7773        let initial_source = "\
7774sketch(on = XY) {
7775  line(start = [var 1, var 2], end = [var 3, var 4])
7776}
7777";
7778
7779        let program = Program::parse(initial_source).unwrap().0.unwrap();
7780
7781        let mut frontend = FrontendState::new();
7782
7783        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7784        let mock_ctx = ExecutorContext::new_mock(None).await;
7785        let version = Version(0);
7786
7787        frontend.hack_set_program(&ctx, program).await.unwrap();
7788        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7789        let sketch_id = sketch_object.id;
7790        let sketch = expect_sketch(sketch_object);
7791        let point_id = *sketch.segments.get(1).unwrap();
7792
7793        let point_ctor = PointCtor {
7794            position: Point2d {
7795                x: Expr::Var(Number {
7796                    value: 5.0,
7797                    units: NumericSuffix::Inch,
7798                }),
7799                y: Expr::Var(Number {
7800                    value: 6.0,
7801                    units: NumericSuffix::Inch,
7802                }),
7803            },
7804        };
7805        let segments = vec![ExistingSegmentCtor {
7806            id: point_id,
7807            ctor: SegmentCtor::Point(point_ctor),
7808        }];
7809        let (src_delta, scene_delta) = frontend
7810            .edit_segments(&mock_ctx, version, sketch_id, segments)
7811            .await
7812            .unwrap();
7813        assert_eq!(
7814            src_delta.text.as_str(),
7815            "\
7816sketch(on = XY) {
7817  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7818}
7819"
7820        );
7821        assert_eq!(scene_delta.new_objects, vec![]);
7822        assert_eq!(
7823            scene_delta.new_graph.objects.len(),
7824            5,
7825            "{:#?}",
7826            scene_delta.new_graph.objects
7827        );
7828
7829        ctx.close().await;
7830        mock_ctx.close().await;
7831    }
7832
7833    #[tokio::test(flavor = "multi_thread")]
7834    async fn test_edit_line_with_coincident_feedback() {
7835        let initial_source = "\
7836sketch(on = XY) {
7837  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7838  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7839  fixed([line1.start, [0, 0]])
7840  coincident([line1.end, line2.start])
7841  equalLength([line1, line2])
7842}
7843";
7844
7845        let program = Program::parse(initial_source).unwrap().0.unwrap();
7846
7847        let mut frontend = FrontendState::new();
7848
7849        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7850        let mock_ctx = ExecutorContext::new_mock(None).await;
7851        let version = Version(0);
7852
7853        frontend.hack_set_program(&ctx, program).await.unwrap();
7854        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7855        let sketch_id = sketch_object.id;
7856        let sketch = expect_sketch(sketch_object);
7857        let line2_end_id = *sketch.segments.get(4).unwrap();
7858
7859        let segments = vec![ExistingSegmentCtor {
7860            id: line2_end_id,
7861            ctor: SegmentCtor::Point(PointCtor {
7862                position: Point2d {
7863                    x: Expr::Var(Number {
7864                        value: 9.0,
7865                        units: NumericSuffix::None,
7866                    }),
7867                    y: Expr::Var(Number {
7868                        value: 10.0,
7869                        units: NumericSuffix::None,
7870                    }),
7871                },
7872            }),
7873        }];
7874        let (src_delta, scene_delta) = frontend
7875            .edit_segments(&mock_ctx, version, sketch_id, segments)
7876            .await
7877            .unwrap();
7878        assert_eq!(
7879            src_delta.text.as_str(),
7880            "\
7881sketch(on = XY) {
7882  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7883  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7884  fixed([line1.start, [0, 0]])
7885  coincident([line1.end, line2.start])
7886  equalLength([line1, line2])
7887}
7888"
7889        );
7890        assert_eq!(
7891            scene_delta.new_graph.objects.len(),
7892            11,
7893            "{:#?}",
7894            scene_delta.new_graph.objects
7895        );
7896
7897        ctx.close().await;
7898        mock_ctx.close().await;
7899    }
7900
7901    #[tokio::test(flavor = "multi_thread")]
7902    async fn test_delete_point_without_var() {
7903        let initial_source = "\
7904sketch(on = XY) {
7905  point(at = [var 1, var 2])
7906  point(at = [var 3, var 4])
7907  point(at = [var 5, var 6])
7908}
7909";
7910
7911        let program = Program::parse(initial_source).unwrap().0.unwrap();
7912
7913        let mut frontend = FrontendState::new();
7914
7915        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7916        let mock_ctx = ExecutorContext::new_mock(None).await;
7917        let version = Version(0);
7918
7919        frontend.hack_set_program(&ctx, program).await.unwrap();
7920        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7921        let sketch_id = sketch_object.id;
7922        let sketch = expect_sketch(sketch_object);
7923
7924        let point_id = *sketch.segments.get(1).unwrap();
7925
7926        let (src_delta, scene_delta) = frontend
7927            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7928            .await
7929            .unwrap();
7930        assert_eq!(
7931            src_delta.text.as_str(),
7932            "\
7933sketch(on = XY) {
7934  point(at = [var 1mm, var 2mm])
7935  point(at = [var 5mm, var 6mm])
7936}
7937"
7938        );
7939        assert_eq!(scene_delta.new_objects, vec![]);
7940        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7941
7942        ctx.close().await;
7943        mock_ctx.close().await;
7944    }
7945
7946    #[tokio::test(flavor = "multi_thread")]
7947    async fn test_delete_point_with_var() {
7948        let initial_source = "\
7949sketch(on = XY) {
7950  point(at = [var 1, var 2])
7951  point1 = point(at = [var 3, var 4])
7952  point(at = [var 5, var 6])
7953}
7954";
7955
7956        let program = Program::parse(initial_source).unwrap().0.unwrap();
7957
7958        let mut frontend = FrontendState::new();
7959
7960        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7961        let mock_ctx = ExecutorContext::new_mock(None).await;
7962        let version = Version(0);
7963
7964        frontend.hack_set_program(&ctx, program).await.unwrap();
7965        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7966        let sketch_id = sketch_object.id;
7967        let sketch = expect_sketch(sketch_object);
7968
7969        let point_id = *sketch.segments.get(1).unwrap();
7970
7971        let (src_delta, scene_delta) = frontend
7972            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7973            .await
7974            .unwrap();
7975        assert_eq!(
7976            src_delta.text.as_str(),
7977            "\
7978sketch(on = XY) {
7979  point(at = [var 1mm, var 2mm])
7980  point(at = [var 5mm, var 6mm])
7981}
7982"
7983        );
7984        assert_eq!(scene_delta.new_objects, vec![]);
7985        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7986
7987        ctx.close().await;
7988        mock_ctx.close().await;
7989    }
7990
7991    #[tokio::test(flavor = "multi_thread")]
7992    async fn test_delete_point_with_var_ignores_stale_warm_starts() {
7993        let initial_source = "\
7994sketch(on = XY) {
7995  point(at = [var 1, var 2])
7996  point1 = point(at = [var 3, var 4])
7997  point(at = [var 5, var 6])
7998}
7999";
8000
8001        let program = Program::parse(initial_source).unwrap().0.unwrap();
8002
8003        let mut frontend = FrontendState::new();
8004        let mock_ctx = ExecutorContext::new_mock(None).await;
8005        let version = Version(0);
8006
8007        frontend.program = program.clone();
8008        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8009        let outcome = frontend.update_state_after_exec(outcome, true);
8010        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8011        let sketch_id = sketch_object.id;
8012        let sketch = expect_sketch(sketch_object);
8013        let point_id = *sketch.segments.get(1).unwrap();
8014        frontend.replace_sketch_var_warm_starts(sketch_id, &outcome);
8015
8016        let (src_delta, _scene_delta) = frontend
8017            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8018            .await
8019            .unwrap();
8020        assert_eq!(
8021            src_delta.text.as_str(),
8022            "\
8023sketch(on = XY) {
8024  point(at = [var 1mm, var 2mm])
8025  point(at = [var 5mm, var 6mm])
8026}
8027"
8028        );
8029
8030        mock_ctx.close().await;
8031    }
8032
8033    #[tokio::test(flavor = "multi_thread")]
8034    async fn test_delete_multiple_points() {
8035        let initial_source = "\
8036sketch(on = XY) {
8037  point(at = [var 1, var 2])
8038  point1 = point(at = [var 3, var 4])
8039  point(at = [var 5, var 6])
8040}
8041";
8042
8043        let program = Program::parse(initial_source).unwrap().0.unwrap();
8044
8045        let mut frontend = FrontendState::new();
8046
8047        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8048        let mock_ctx = ExecutorContext::new_mock(None).await;
8049        let version = Version(0);
8050
8051        frontend.hack_set_program(&ctx, program).await.unwrap();
8052        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8053        let sketch_id = sketch_object.id;
8054
8055        let sketch = expect_sketch(sketch_object);
8056
8057        let point1_id = *sketch.segments.first().unwrap();
8058        let point2_id = *sketch.segments.get(1).unwrap();
8059
8060        let (src_delta, scene_delta) = frontend
8061            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
8062            .await
8063            .unwrap();
8064        assert_eq!(
8065            src_delta.text.as_str(),
8066            "\
8067sketch(on = XY) {
8068  point(at = [var 5mm, var 6mm])
8069}
8070"
8071        );
8072        assert_eq!(scene_delta.new_objects, vec![]);
8073        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8074
8075        ctx.close().await;
8076        mock_ctx.close().await;
8077    }
8078
8079    #[tokio::test(flavor = "multi_thread")]
8080    async fn test_delete_coincident_constraint() {
8081        let initial_source = "\
8082sketch(on = XY) {
8083  point1 = point(at = [var 1, var 2])
8084  point2 = point(at = [var 3, var 4])
8085  coincident([point1, point2])
8086  point(at = [var 5, var 6])
8087}
8088";
8089
8090        let program = Program::parse(initial_source).unwrap().0.unwrap();
8091
8092        let mut frontend = FrontendState::new();
8093
8094        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8095        let mock_ctx = ExecutorContext::new_mock(None).await;
8096        let version = Version(0);
8097
8098        frontend.hack_set_program(&ctx, program).await.unwrap();
8099        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8100        let sketch_id = sketch_object.id;
8101        let sketch = expect_sketch(sketch_object);
8102
8103        let coincident_id = *sketch.constraints.first().unwrap();
8104
8105        let (src_delta, scene_delta) = frontend
8106            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8107            .await
8108            .unwrap();
8109        assert_eq!(
8110            src_delta.text.as_str(),
8111            "\
8112sketch(on = XY) {
8113  point1 = point(at = [var 1mm, var 2mm])
8114  point2 = point(at = [var 3mm, var 4mm])
8115  point(at = [var 5mm, var 6mm])
8116}
8117"
8118        );
8119        assert_eq!(scene_delta.new_objects, vec![]);
8120        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8121
8122        ctx.close().await;
8123        mock_ctx.close().await;
8124    }
8125
8126    #[tokio::test(flavor = "multi_thread")]
8127    async fn test_delete_line_cascades_to_coincident_constraint() {
8128        let initial_source = "\
8129sketch(on = XY) {
8130  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8131  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8132  coincident([line1.end, line2.start])
8133}
8134";
8135
8136        let program = Program::parse(initial_source).unwrap().0.unwrap();
8137
8138        let mut frontend = FrontendState::new();
8139
8140        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8141        let mock_ctx = ExecutorContext::new_mock(None).await;
8142        let version = Version(0);
8143
8144        frontend.hack_set_program(&ctx, program).await.unwrap();
8145        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8146        let sketch_id = sketch_object.id;
8147        let sketch = expect_sketch(sketch_object);
8148        let line_id = *sketch.segments.get(5).unwrap();
8149
8150        let (src_delta, scene_delta) = frontend
8151            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8152            .await
8153            .unwrap();
8154        assert_eq!(
8155            src_delta.text.as_str(),
8156            "\
8157sketch(on = XY) {
8158  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8159}
8160"
8161        );
8162        assert_eq!(
8163            scene_delta.new_graph.objects.len(),
8164            5,
8165            "{:#?}",
8166            scene_delta.new_graph.objects
8167        );
8168
8169        ctx.close().await;
8170        mock_ctx.close().await;
8171    }
8172
8173    #[tokio::test(flavor = "multi_thread")]
8174    async fn test_delete_line_cascades_to_distance_constraint() {
8175        let initial_source = "\
8176sketch(on = XY) {
8177  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8178  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8179  distance([line1.end, line2.start]) == 10mm
8180}
8181";
8182
8183        let program = Program::parse(initial_source).unwrap().0.unwrap();
8184
8185        let mut frontend = FrontendState::new();
8186
8187        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8188        let mock_ctx = ExecutorContext::new_mock(None).await;
8189        let version = Version(0);
8190
8191        frontend.hack_set_program(&ctx, program).await.unwrap();
8192        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8193        let sketch_id = sketch_object.id;
8194        let sketch = expect_sketch(sketch_object);
8195        let line_id = *sketch.segments.get(5).unwrap();
8196
8197        let (src_delta, scene_delta) = frontend
8198            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8199            .await
8200            .unwrap();
8201        assert_eq!(
8202            src_delta.text.as_str(),
8203            "\
8204sketch(on = XY) {
8205  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8206}
8207"
8208        );
8209        assert_eq!(
8210            scene_delta.new_graph.objects.len(),
8211            5,
8212            "{:#?}",
8213            scene_delta.new_graph.objects
8214        );
8215
8216        ctx.close().await;
8217        mock_ctx.close().await;
8218    }
8219
8220    #[tokio::test(flavor = "multi_thread")]
8221    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
8222        let initial_source = "\
8223sketch(on = XY) {
8224  point1 = point(at = [var 1, var 2])
8225  point2 = point(at = [var 3, var 4])
8226  horizontalDistance([point1, point2]) == 10mm
8227}
8228";
8229
8230        let program = Program::parse(initial_source).unwrap().0.unwrap();
8231
8232        let mut frontend = FrontendState::new();
8233
8234        let mock_ctx = ExecutorContext::new_mock(None).await;
8235        let version = Version(0);
8236
8237        frontend.program = program.clone();
8238        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8239        frontend.update_state_after_exec(outcome, true);
8240        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8241        let sketch_id = sketch_object.id;
8242        let sketch = expect_sketch(sketch_object);
8243        let point2_id = *sketch.segments.get(1).unwrap();
8244
8245        let (src_delta, scene_delta) = frontend
8246            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
8247            .await
8248            .unwrap();
8249        assert_eq!(
8250            src_delta.text.as_str(),
8251            "\
8252sketch(on = XY) {
8253  point1 = point(at = [var 1mm, var 2mm])
8254}
8255"
8256        );
8257        assert_eq!(
8258            scene_delta.new_graph.objects.len(),
8259            3,
8260            "{:#?}",
8261            scene_delta.new_graph.objects
8262        );
8263
8264        mock_ctx.close().await;
8265    }
8266
8267    #[tokio::test(flavor = "multi_thread")]
8268    async fn test_delete_line_cascades_to_fixed_constraint() {
8269        let initial_source = "\
8270sketch(on = XY) {
8271  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8272  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8273  fixed([line1.start, [0, 0]])
8274}
8275";
8276
8277        let program = Program::parse(initial_source).unwrap().0.unwrap();
8278
8279        let mut frontend = FrontendState::new();
8280
8281        let mock_ctx = ExecutorContext::new_mock(None).await;
8282        let version = Version(0);
8283
8284        frontend.program = program.clone();
8285        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8286        frontend.update_state_after_exec(outcome, true);
8287        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8288        let sketch_id = sketch_object.id;
8289        let sketch = expect_sketch(sketch_object);
8290        let line1_id = *sketch.segments.get(2).unwrap();
8291
8292        let (src_delta, scene_delta) = frontend
8293            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8294            .await
8295            .unwrap();
8296        assert_eq!(
8297            src_delta.text.as_str(),
8298            "\
8299sketch(on = XY) {
8300  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8301}
8302"
8303        );
8304        assert_eq!(
8305            scene_delta.new_graph.objects.len(),
8306            5,
8307            "{:#?}",
8308            scene_delta.new_graph.objects
8309        );
8310
8311        mock_ctx.close().await;
8312    }
8313
8314    #[tokio::test(flavor = "multi_thread")]
8315    async fn test_delete_line_cascades_to_midpoint_constraint() {
8316        let initial_source = "\
8317sketch(on = XY) {
8318  point1 = point(at = [var 1, var 2])
8319  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8320  midpoint(line1, point = point1)
8321}
8322";
8323
8324        let program = Program::parse(initial_source).unwrap().0.unwrap();
8325
8326        let mut frontend = FrontendState::new();
8327
8328        let mock_ctx = ExecutorContext::new_mock(None).await;
8329        let version = Version(0);
8330
8331        frontend.program = program.clone();
8332        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8333        frontend.update_state_after_exec(outcome, true);
8334        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8335        let sketch_id = sketch_object.id;
8336        let sketch = expect_sketch(sketch_object);
8337        let line1_id = *sketch.segments.get(3).unwrap();
8338
8339        let (src_delta, scene_delta) = frontend
8340            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8341            .await
8342            .unwrap();
8343        assert_eq!(
8344            src_delta.text.as_str(),
8345            "\
8346sketch(on = XY) {
8347  point1 = point(at = [var 1mm, var 2mm])
8348}
8349"
8350        );
8351        assert_eq!(
8352            scene_delta.new_graph.objects.len(),
8353            3,
8354            "{:#?}",
8355            scene_delta.new_graph.objects
8356        );
8357
8358        mock_ctx.close().await;
8359    }
8360
8361    #[tokio::test(flavor = "multi_thread")]
8362    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8363        let initial_source = "\
8364sketch(on = XY) {
8365  point1 = point(at = [var 1, var 2])
8366  point2 = point(at = [var 3, var 4])
8367  point3 = point(at = [var 5, var 6])
8368  coincident([point1, point2, point3])
8369}
8370";
8371
8372        let program = Program::parse(initial_source).unwrap().0.unwrap();
8373
8374        let mut frontend = FrontendState::new();
8375
8376        let mock_ctx = ExecutorContext::new_mock(None).await;
8377        let version = Version(0);
8378
8379        frontend.program = program.clone();
8380        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8381        frontend.update_state_after_exec(outcome, true);
8382        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8383        let sketch_id = sketch_object.id;
8384        let sketch = expect_sketch(sketch_object);
8385        let point3_id = *sketch.segments.get(2).unwrap();
8386
8387        let (src_delta, scene_delta) = frontend
8388            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8389            .await
8390            .unwrap();
8391        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8392        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8393        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8394        assert!(
8395            src_delta.text.contains("coincident([point1, point2])"),
8396            "{}",
8397            src_delta.text
8398        );
8399
8400        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8401        let sketch = expect_sketch(sketch_object);
8402        assert_eq!(sketch.segments.len(), 2);
8403        assert_eq!(sketch.constraints.len(), 1);
8404
8405        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8406        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8407            panic!("Expected constraint object");
8408        };
8409        let Constraint::Coincident(coincident) = constraint else {
8410            panic!("Expected coincident constraint");
8411        };
8412        assert_eq!(
8413            coincident.segments,
8414            sketch
8415                .segments
8416                .iter()
8417                .copied()
8418                .map(Into::into)
8419                .collect::<Vec<ConstraintSegment>>()
8420        );
8421
8422        mock_ctx.close().await;
8423    }
8424
8425    #[tokio::test(flavor = "multi_thread")]
8426    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8427        let initial_source = "\
8428sketch(on = XY) {
8429  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8430  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8431  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8432  equalLength([line1, line2, line3])
8433}
8434";
8435
8436        let program = Program::parse(initial_source).unwrap().0.unwrap();
8437
8438        let mut frontend = FrontendState::new();
8439
8440        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8441        let mock_ctx = ExecutorContext::new_mock(None).await;
8442        let version = Version(0);
8443
8444        frontend.hack_set_program(&ctx, program).await.unwrap();
8445        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8446        let sketch_id = sketch_object.id;
8447        let sketch = expect_sketch(sketch_object);
8448        let line3_id = *sketch.segments.get(8).unwrap();
8449
8450        let (src_delta, scene_delta) = frontend
8451            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8452            .await
8453            .unwrap();
8454        assert_eq!(
8455            src_delta.text.as_str(),
8456            "\
8457sketch(on = XY) {
8458  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8459  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8460  equalLength([line1, line2])
8461}
8462"
8463        );
8464
8465        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8466        let sketch = expect_sketch(sketch_object);
8467        assert_eq!(sketch.constraints.len(), 1);
8468
8469        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8470        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8471            panic!("Expected constraint object");
8472        };
8473        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8474            panic!("Expected lines equal length constraint");
8475        };
8476        assert_eq!(lines_equal_length.lines.len(), 2);
8477
8478        ctx.close().await;
8479        mock_ctx.close().await;
8480    }
8481
8482    #[tokio::test(flavor = "multi_thread")]
8483    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8484        let initial_source = "\
8485sketch(on = XY) {
8486  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8487  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8488  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8489  horizontal([line1.end, line2.start, line3.start])
8490}
8491";
8492
8493        let program = Program::parse(initial_source).unwrap().0.unwrap();
8494
8495        let mut frontend = FrontendState::new();
8496
8497        let mock_ctx = ExecutorContext::new_mock(None).await;
8498        let version = Version(0);
8499
8500        frontend.program = program.clone();
8501        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8502        frontend.update_state_after_exec(outcome, true);
8503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8504        let sketch_id = sketch_object.id;
8505        let sketch = expect_sketch(sketch_object);
8506        let line1_id = *sketch.segments.get(2).unwrap();
8507
8508        let (src_delta, scene_delta) = frontend
8509            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8510            .await
8511            .unwrap();
8512        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8513        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8514        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8515        assert!(
8516            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8517            "{}",
8518            src_delta.text
8519        );
8520
8521        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8522        let sketch = expect_sketch(sketch_object);
8523        assert_eq!(sketch.constraints.len(), 1);
8524
8525        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8526        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8527            panic!("Expected constraint object");
8528        };
8529        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8530            panic!("Expected horizontal points constraint");
8531        };
8532        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8533        assert_eq!(*points, remaining_points);
8534
8535        mock_ctx.close().await;
8536    }
8537
8538    #[tokio::test(flavor = "multi_thread")]
8539    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8540        let initial_source = "\
8541sketch(on = XY) {
8542  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8543  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8544  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8545  vertical([line1.end, line2.start, line3.start])
8546}
8547";
8548
8549        let program = Program::parse(initial_source).unwrap().0.unwrap();
8550
8551        let mut frontend = FrontendState::new();
8552
8553        let mock_ctx = ExecutorContext::new_mock(None).await;
8554        let version = Version(0);
8555
8556        frontend.program = program.clone();
8557        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8558        frontend.update_state_after_exec(outcome, true);
8559        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8560        let sketch_id = sketch_object.id;
8561        let sketch = expect_sketch(sketch_object);
8562        let line1_id = *sketch.segments.get(2).unwrap();
8563
8564        let (src_delta, scene_delta) = frontend
8565            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8566            .await
8567            .unwrap();
8568        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8569        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8570        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8571        assert!(
8572            src_delta.text.contains("vertical([line2.start, line3.start])"),
8573            "{}",
8574            src_delta.text
8575        );
8576
8577        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8578        let sketch = expect_sketch(sketch_object);
8579        assert_eq!(sketch.constraints.len(), 1);
8580
8581        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8582        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8583            panic!("Expected constraint object");
8584        };
8585        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8586            panic!("Expected vertical points constraint");
8587        };
8588        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8589        assert_eq!(*points, remaining_points);
8590
8591        mock_ctx.close().await;
8592    }
8593
8594    #[tokio::test(flavor = "multi_thread")]
8595    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8596        let initial_source = "\
8597sketch(on = XY) {
8598  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8599  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8600  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8601  coincident([line1.end, line2.start, line3.start])
8602}
8603";
8604
8605        let program = Program::parse(initial_source).unwrap().0.unwrap();
8606
8607        let mut frontend = FrontendState::new();
8608
8609        let mock_ctx = ExecutorContext::new_mock(None).await;
8610        let version = Version(0);
8611
8612        frontend.program = program.clone();
8613        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8614        frontend.update_state_after_exec(outcome, true);
8615        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8616        let sketch_id = sketch_object.id;
8617        let sketch = expect_sketch(sketch_object);
8618        let line1_id = *sketch.segments.get(2).unwrap();
8619
8620        let (src_delta, scene_delta) = frontend
8621            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8622            .await
8623            .unwrap();
8624        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8625        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8626        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8627        assert!(
8628            src_delta.text.contains("coincident([line2.start, line3.start])"),
8629            "{}",
8630            src_delta.text
8631        );
8632
8633        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8634        let sketch = expect_sketch(sketch_object);
8635        assert_eq!(sketch.constraints.len(), 1);
8636
8637        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8638        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8639            panic!("Expected constraint object");
8640        };
8641        let Constraint::Coincident(coincident) = constraint else {
8642            panic!("Expected coincident constraint");
8643        };
8644        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8645        assert_eq!(coincident.segments, remaining_segments);
8646
8647        mock_ctx.close().await;
8648    }
8649
8650    #[tokio::test(flavor = "multi_thread")]
8651    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8652        let initial_source = "\
8653sketch(on = XY) {
8654  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8655  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8656  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8657  equalLength([line1, line2, line3])
8658}
8659";
8660
8661        let program = Program::parse(initial_source).unwrap().0.unwrap();
8662
8663        let mut frontend = FrontendState::new();
8664
8665        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8666        let mock_ctx = ExecutorContext::new_mock(None).await;
8667        let version = Version(0);
8668
8669        frontend.hack_set_program(&ctx, program).await.unwrap();
8670        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8671        let sketch_id = sketch_object.id;
8672        let sketch = expect_sketch(sketch_object);
8673        let line2_id = *sketch.segments.get(5).unwrap();
8674        let line3_id = *sketch.segments.get(8).unwrap();
8675
8676        let (src_delta, scene_delta) = frontend
8677            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8678            .await
8679            .unwrap();
8680        assert_eq!(
8681            src_delta.text.as_str(),
8682            "\
8683sketch(on = XY) {
8684  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8685}
8686"
8687        );
8688
8689        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8690        let sketch = expect_sketch(sketch_object);
8691        assert!(sketch.constraints.is_empty());
8692
8693        ctx.close().await;
8694        mock_ctx.close().await;
8695    }
8696
8697    #[tokio::test(flavor = "multi_thread")]
8698    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8699        let initial_source = "\
8700sketch(on = XY) {
8701  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8702  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8703  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8704  parallel([line1, line2, line3])
8705}
8706";
8707
8708        let program = Program::parse(initial_source).unwrap().0.unwrap();
8709
8710        let mut frontend = FrontendState::new();
8711
8712        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8713        let mock_ctx = ExecutorContext::new_mock(None).await;
8714        let version = Version(0);
8715
8716        frontend.hack_set_program(&ctx, program).await.unwrap();
8717        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8718        let sketch_id = sketch_object.id;
8719        let sketch = expect_sketch(sketch_object);
8720        let line3_id = *sketch.segments.get(8).unwrap();
8721
8722        let (src_delta, scene_delta) = frontend
8723            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8724            .await
8725            .unwrap();
8726        assert_eq!(
8727            src_delta.text.as_str(),
8728            "\
8729sketch(on = XY) {
8730  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8731  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8732  parallel([line1, line2])
8733}
8734"
8735        );
8736
8737        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8738        let sketch = expect_sketch(sketch_object);
8739        assert_eq!(sketch.constraints.len(), 1);
8740
8741        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8742        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8743            panic!("Expected constraint object");
8744        };
8745        let Constraint::Parallel(parallel) = constraint else {
8746            panic!("Expected parallel constraint");
8747        };
8748        assert_eq!(parallel.lines.len(), 2);
8749
8750        ctx.close().await;
8751        mock_ctx.close().await;
8752    }
8753
8754    #[tokio::test(flavor = "multi_thread")]
8755    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8756        let initial_source = "\
8757sketch(on = XY) {
8758  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8759  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8760  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8761  parallel([line1, line2, line3])
8762}
8763";
8764
8765        let program = Program::parse(initial_source).unwrap().0.unwrap();
8766
8767        let mut frontend = FrontendState::new();
8768
8769        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8770        let mock_ctx = ExecutorContext::new_mock(None).await;
8771        let version = Version(0);
8772
8773        frontend.hack_set_program(&ctx, program).await.unwrap();
8774        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8775        let sketch_id = sketch_object.id;
8776        let sketch = expect_sketch(sketch_object);
8777        let line2_id = *sketch.segments.get(5).unwrap();
8778        let line3_id = *sketch.segments.get(8).unwrap();
8779
8780        let (src_delta, scene_delta) = frontend
8781            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8782            .await
8783            .unwrap();
8784        assert_eq!(
8785            src_delta.text.as_str(),
8786            "\
8787sketch(on = XY) {
8788  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8789}
8790"
8791        );
8792
8793        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8794        let sketch = expect_sketch(sketch_object);
8795        assert!(sketch.constraints.is_empty());
8796
8797        ctx.close().await;
8798        mock_ctx.close().await;
8799    }
8800
8801    #[tokio::test(flavor = "multi_thread")]
8802    async fn test_delete_line_line_coincident_constraint() {
8803        let initial_source = "\
8804sketch(on = XY) {
8805  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8806  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8807  coincident([line1, line2])
8808}
8809";
8810
8811        let program = Program::parse(initial_source).unwrap().0.unwrap();
8812
8813        let mut frontend = FrontendState::new();
8814
8815        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8816        let mock_ctx = ExecutorContext::new_mock(None).await;
8817        let version = Version(0);
8818
8819        frontend.hack_set_program(&ctx, program).await.unwrap();
8820        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8821        let sketch_id = sketch_object.id;
8822        let sketch = expect_sketch(sketch_object);
8823
8824        let coincident_id = *sketch.constraints.first().unwrap();
8825
8826        let (src_delta, scene_delta) = frontend
8827            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8828            .await
8829            .unwrap();
8830        assert_eq!(
8831            src_delta.text.as_str(),
8832            "\
8833sketch(on = XY) {
8834  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8835  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8836}
8837"
8838        );
8839        assert_eq!(scene_delta.new_objects, vec![]);
8840        assert_eq!(scene_delta.new_graph.objects.len(), 8);
8841
8842        ctx.close().await;
8843        mock_ctx.close().await;
8844    }
8845
8846    #[tokio::test(flavor = "multi_thread")]
8847    async fn test_two_points_coincident() {
8848        let initial_source = "\
8849sketch(on = XY) {
8850  point1 = point(at = [var 1, var 2])
8851  point(at = [3, 4])
8852}
8853";
8854
8855        let program = Program::parse(initial_source).unwrap().0.unwrap();
8856
8857        let mut frontend = FrontendState::new();
8858
8859        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8860        let mock_ctx = ExecutorContext::new_mock(None).await;
8861        let version = Version(0);
8862
8863        frontend.hack_set_program(&ctx, program).await.unwrap();
8864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8865        let sketch_id = sketch_object.id;
8866        let sketch = expect_sketch(sketch_object);
8867        let point0_id = *sketch.segments.first().unwrap();
8868        let point1_id = *sketch.segments.get(1).unwrap();
8869
8870        let constraint = Constraint::Coincident(Coincident {
8871            segments: vec![point0_id.into(), point1_id.into()],
8872        });
8873        let (src_delta, scene_delta) = frontend
8874            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8875            .await
8876            .unwrap();
8877        assert_eq!(
8878            src_delta.text.as_str(),
8879            "\
8880sketch(on = XY) {
8881  point1 = point(at = [var 1, var 2])
8882  point2 = point(at = [3, 4])
8883  coincident([point1, point2])
8884}
8885"
8886        );
8887        assert_eq!(
8888            scene_delta.new_graph.objects.len(),
8889            5,
8890            "{:#?}",
8891            scene_delta.new_graph.objects
8892        );
8893
8894        ctx.close().await;
8895        mock_ctx.close().await;
8896    }
8897
8898    #[tokio::test(flavor = "multi_thread")]
8899    async fn test_three_points_coincident() {
8900        let initial_source = "\
8901sketch(on = XY) {
8902  point1 = point(at = [var 1, var 2])
8903  point(at = [var 3, var 4])
8904  point(at = [var 5, var 6])
8905}
8906";
8907
8908        let program = Program::parse(initial_source).unwrap().0.unwrap();
8909
8910        let mut frontend = FrontendState::new();
8911
8912        let mock_ctx = ExecutorContext::new_mock(None).await;
8913        let version = Version(0);
8914
8915        frontend.program = program.clone();
8916        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8917        frontend.update_state_after_exec(outcome, true);
8918        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8919        let sketch_id = sketch_object.id;
8920        let sketch = expect_sketch(sketch_object);
8921        let segments = sketch
8922            .segments
8923            .iter()
8924            .take(3)
8925            .copied()
8926            .map(Into::into)
8927            .collect::<Vec<ConstraintSegment>>();
8928
8929        let constraint = Constraint::Coincident(Coincident {
8930            segments: segments.clone(),
8931        });
8932        let (src_delta, scene_delta) = frontend
8933            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8934            .await
8935            .unwrap();
8936        assert_eq!(
8937            src_delta.text.as_str(),
8938            "\
8939sketch(on = XY) {
8940  point1 = point(at = [var 1, var 2])
8941  point2 = point(at = [var 3, var 4])
8942  point3 = point(at = [var 5, var 6])
8943  coincident([point1, point2, point3])
8944}
8945"
8946        );
8947
8948        let constraint_object = scene_delta
8949            .new_graph
8950            .objects
8951            .iter()
8952            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8953            .unwrap();
8954
8955        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8956            panic!("expected a constraint object");
8957        };
8958
8959        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8960
8961        mock_ctx.close().await;
8962    }
8963
8964    #[tokio::test(flavor = "multi_thread")]
8965    async fn test_source_with_three_point_coincident_tracks_all_segments() {
8966        let initial_source = "\
8967sketch(on = XY) {
8968  point1 = point(at = [var 1, var 2])
8969  point2 = point(at = [var 3, var 4])
8970  point3 = point(at = [var 5, var 6])
8971  coincident([point1, point2, point3])
8972}
8973";
8974
8975        let program = Program::parse(initial_source).unwrap().0.unwrap();
8976
8977        let mut frontend = FrontendState::new();
8978
8979        let ctx = ExecutorContext::new_mock(None).await;
8980        frontend.program = program.clone();
8981        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8982        frontend.update_state_after_exec(outcome, true);
8983
8984        let constraint_object = frontend
8985            .scene_graph
8986            .objects
8987            .iter()
8988            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8989            .unwrap();
8990        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8991            panic!("expected a constraint object");
8992        };
8993
8994        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8995        let sketch = expect_sketch(sketch_object);
8996        let expected_segments = sketch
8997            .segments
8998            .iter()
8999            .take(3)
9000            .copied()
9001            .map(Into::into)
9002            .collect::<Vec<ConstraintSegment>>();
9003
9004        assert_eq!(
9005            constraint,
9006            &Constraint::Coincident(Coincident {
9007                segments: expected_segments,
9008            })
9009        );
9010
9011        ctx.close().await;
9012    }
9013
9014    #[tokio::test(flavor = "multi_thread")]
9015    async fn test_point_origin_coincident_preserves_order() {
9016        let initial_source = "\
9017sketch(on = XY) {
9018  point(at = [var 1, var 2])
9019}
9020";
9021
9022        for (origin_first, expected_source) in [
9023            (
9024                true,
9025                "\
9026sketch(on = XY) {
9027  point1 = point(at = [var 1, var 2])
9028  coincident([ORIGIN, point1])
9029}
9030",
9031            ),
9032            (
9033                false,
9034                "\
9035sketch(on = XY) {
9036  point1 = point(at = [var 1, var 2])
9037  coincident([point1, ORIGIN])
9038}
9039",
9040            ),
9041        ] {
9042            let program = Program::parse(initial_source).unwrap().0.unwrap();
9043
9044            let mut frontend = FrontendState::new();
9045
9046            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9047            let mock_ctx = ExecutorContext::new_mock(None).await;
9048            let version = Version(0);
9049
9050            frontend.hack_set_program(&ctx, program).await.unwrap();
9051            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9052            let sketch_id = sketch_object.id;
9053            let sketch = expect_sketch(sketch_object);
9054            let point_id = *sketch.segments.first().unwrap();
9055
9056            let segments = if origin_first {
9057                vec![ConstraintSegment::ORIGIN, point_id.into()]
9058            } else {
9059                vec![point_id.into(), ConstraintSegment::ORIGIN]
9060            };
9061            let constraint = Constraint::Coincident(Coincident {
9062                segments: segments.clone(),
9063            });
9064            let (src_delta, scene_delta) = frontend
9065                .add_constraint(&mock_ctx, version, sketch_id, constraint)
9066                .await
9067                .unwrap();
9068            assert_eq!(src_delta.text.as_str(), expected_source);
9069
9070            let constraint_object = scene_delta
9071                .new_graph
9072                .objects
9073                .iter()
9074                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9075                .unwrap();
9076
9077            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9078                panic!("expected a constraint object");
9079            };
9080
9081            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9082
9083            ctx.close().await;
9084            mock_ctx.close().await;
9085        }
9086    }
9087
9088    #[tokio::test(flavor = "multi_thread")]
9089    async fn test_coincident_of_line_end_points() {
9090        let initial_source = "\
9091sketch(on = XY) {
9092  line(start = [var 1, var 2], end = [var 3, var 4])
9093  line(start = [var 5, var 6], end = [var 7, var 8])
9094}
9095";
9096
9097        let program = Program::parse(initial_source).unwrap().0.unwrap();
9098
9099        let mut frontend = FrontendState::new();
9100
9101        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9102        let mock_ctx = ExecutorContext::new_mock(None).await;
9103        let version = Version(0);
9104
9105        frontend.hack_set_program(&ctx, program).await.unwrap();
9106        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9107        let sketch_id = sketch_object.id;
9108        let sketch = expect_sketch(sketch_object);
9109        let point0_id = *sketch.segments.get(1).unwrap();
9110        let point1_id = *sketch.segments.get(3).unwrap();
9111
9112        let constraint = Constraint::Coincident(Coincident {
9113            segments: vec![point0_id.into(), point1_id.into()],
9114        });
9115        let (src_delta, scene_delta) = frontend
9116            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9117            .await
9118            .unwrap();
9119        assert_eq!(
9120            src_delta.text.as_str(),
9121            "\
9122sketch(on = XY) {
9123  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9124  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9125  coincident([line1.end, line2.start])
9126}
9127"
9128        );
9129        assert_eq!(
9130            scene_delta.new_graph.objects.len(),
9131            9,
9132            "{:#?}",
9133            scene_delta.new_graph.objects
9134        );
9135
9136        ctx.close().await;
9137        mock_ctx.close().await;
9138    }
9139
9140    #[tokio::test(flavor = "multi_thread")]
9141    async fn test_coincident_of_line_point_and_circle_segment() {
9142        let initial_source = "\
9143sketch(on = XY) {
9144  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9145  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9146}
9147";
9148        let program = Program::parse(initial_source).unwrap().0.unwrap();
9149        let mut frontend = FrontendState::new();
9150
9151        let mock_ctx = ExecutorContext::new_mock(None).await;
9152        let version = Version(0);
9153
9154        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9155        frontend.program = program;
9156        frontend.update_state_after_exec(outcome, true);
9157        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
9158        let sketch_id = sketch_object.id;
9159        let sketch = expect_sketch(sketch_object);
9160
9161        let circle_id = sketch
9162            .segments
9163            .iter()
9164            .copied()
9165            .find(|seg_id| {
9166                matches!(
9167                    &frontend.scene_graph.objects[seg_id.0].kind,
9168                    ObjectKind::Segment {
9169                        segment: Segment::Circle(_)
9170                    }
9171                )
9172            })
9173            .expect("Expected a circle segment in sketch");
9174        let line_id = sketch
9175            .segments
9176            .iter()
9177            .copied()
9178            .find(|seg_id| {
9179                matches!(
9180                    &frontend.scene_graph.objects[seg_id.0].kind,
9181                    ObjectKind::Segment {
9182                        segment: Segment::Line(_)
9183                    }
9184                )
9185            })
9186            .expect("Expected a line segment in sketch");
9187
9188        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
9189            ObjectKind::Segment {
9190                segment: Segment::Line(line),
9191            } => line.start,
9192            _ => panic!("Expected line segment object"),
9193        };
9194
9195        let constraint = Constraint::Coincident(Coincident {
9196            segments: vec![line_start_point_id.into(), circle_id.into()],
9197        });
9198        let (src_delta, _scene_delta) = frontend
9199            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9200            .await
9201            .unwrap();
9202        assert_eq!(
9203            src_delta.text.as_str(),
9204            "\
9205sketch(on = XY) {
9206  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9207  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9208  coincident([line1.start, circle1])
9209}
9210"
9211        );
9212
9213        mock_ctx.close().await;
9214    }
9215
9216    #[tokio::test(flavor = "multi_thread")]
9217    async fn test_invalid_coincident_arc_and_line_preserves_state() {
9218        // Test that attempting an invalid coincident constraint (arc and line)
9219        // doesn't corrupt the state, allowing subsequent operations to work.
9220        // This test verifies the transactional fix in add_constraint that prevents
9221        // state corruption when invalid constraints are attempted.
9222        // Example: coincident constraint between an arc segment and a straight line segment
9223        // is geometrically invalid and should fail, but state should remain intact.
9224        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
9225        let program = Program::empty();
9226
9227        let mut frontend = FrontendState::new();
9228        frontend.program = program;
9229
9230        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9231        let mock_ctx = ExecutorContext::new_mock(None).await;
9232        let version = Version(0);
9233
9234        let sketch_args = SketchCtor {
9235            on: Plane::Default(PlaneName::Xy),
9236        };
9237        let (_src_delta, _scene_delta, sketch_id) = frontend
9238            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9239            .await
9240            .unwrap();
9241
9242        // Add an arc segment
9243        let arc_ctor = ArcCtor {
9244            start: Point2d {
9245                x: Expr::Var(Number {
9246                    value: 0.0,
9247                    units: NumericSuffix::Mm,
9248                }),
9249                y: Expr::Var(Number {
9250                    value: 0.0,
9251                    units: NumericSuffix::Mm,
9252                }),
9253            },
9254            end: Point2d {
9255                x: Expr::Var(Number {
9256                    value: 10.0,
9257                    units: NumericSuffix::Mm,
9258                }),
9259                y: Expr::Var(Number {
9260                    value: 10.0,
9261                    units: NumericSuffix::Mm,
9262                }),
9263            },
9264            center: Point2d {
9265                x: Expr::Var(Number {
9266                    value: 10.0,
9267                    units: NumericSuffix::Mm,
9268                }),
9269                y: Expr::Var(Number {
9270                    value: 0.0,
9271                    units: NumericSuffix::Mm,
9272                }),
9273            },
9274            construction: None,
9275        };
9276        let (_src_delta, scene_delta) = frontend
9277            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
9278            .await
9279            .unwrap();
9280        // The arc is the last object in new_objects (after the 3 points: start, end, center)
9281        let arc_id = *scene_delta.new_objects.last().unwrap();
9282
9283        // Add a line segment
9284        let line_ctor = LineCtor {
9285            start: Point2d {
9286                x: Expr::Var(Number {
9287                    value: 20.0,
9288                    units: NumericSuffix::Mm,
9289                }),
9290                y: Expr::Var(Number {
9291                    value: 0.0,
9292                    units: NumericSuffix::Mm,
9293                }),
9294            },
9295            end: Point2d {
9296                x: Expr::Var(Number {
9297                    value: 30.0,
9298                    units: NumericSuffix::Mm,
9299                }),
9300                y: Expr::Var(Number {
9301                    value: 10.0,
9302                    units: NumericSuffix::Mm,
9303                }),
9304            },
9305            construction: None,
9306        };
9307        let (_src_delta, scene_delta) = frontend
9308            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9309            .await
9310            .unwrap();
9311        // The line is the last object in new_objects (after the 2 points: start, end)
9312        let line_id = *scene_delta.new_objects.last().unwrap();
9313
9314        // Attempt to add an invalid coincident constraint between arc and line
9315        // This should fail during execution, but state should remain intact
9316        let constraint = Constraint::Coincident(Coincident {
9317            segments: vec![arc_id.into(), line_id.into()],
9318        });
9319        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9320
9321        // The constraint addition should fail (invalid constraint)
9322        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9323
9324        // Verify state is not corrupted by checking that we can still access the scene graph
9325        // and that the original segments are still present with their source ranges
9326        let sketch_object_after =
9327            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9328        let sketch_after = expect_sketch(sketch_object_after);
9329
9330        // Verify both segments are still in the sketch
9331        assert!(
9332            sketch_after.segments.contains(&arc_id),
9333            "Arc segment should still exist after failed constraint"
9334        );
9335        assert!(
9336            sketch_after.segments.contains(&line_id),
9337            "Line segment should still exist after failed constraint"
9338        );
9339
9340        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
9341        let arc_obj = frontend
9342            .scene_graph
9343            .objects
9344            .get(arc_id.0)
9345            .expect("Arc object should still be accessible");
9346        let line_obj = frontend
9347            .scene_graph
9348            .objects
9349            .get(line_id.0)
9350            .expect("Line object should still be accessible");
9351
9352        // Verify source ranges are still valid (not corrupted)
9353        // Just verify that the objects are still accessible and have the expected types
9354        match &arc_obj.kind {
9355            ObjectKind::Segment {
9356                segment: Segment::Arc(_),
9357            } => {}
9358            _ => panic!("Arc object should still be an arc segment"),
9359        }
9360        match &line_obj.kind {
9361            ObjectKind::Segment {
9362                segment: Segment::Line(_),
9363            } => {}
9364            _ => panic!("Line object should still be a line segment"),
9365        }
9366
9367        ctx.close().await;
9368        mock_ctx.close().await;
9369    }
9370
9371    #[tokio::test(flavor = "multi_thread")]
9372    async fn test_distance_two_points() {
9373        let initial_source = "\
9374sketch(on = XY) {
9375  point(at = [var 1, var 2])
9376  point(at = [var 3, var 4])
9377}
9378";
9379
9380        let program = Program::parse(initial_source).unwrap().0.unwrap();
9381
9382        let mut frontend = FrontendState::new();
9383
9384        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9385        let mock_ctx = ExecutorContext::new_mock(None).await;
9386        let version = Version(0);
9387
9388        frontend.hack_set_program(&ctx, program).await.unwrap();
9389        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9390        let sketch_id = sketch_object.id;
9391        let sketch = expect_sketch(sketch_object);
9392        let point0_id = *sketch.segments.first().unwrap();
9393        let point1_id = *sketch.segments.get(1).unwrap();
9394
9395        let constraint = Constraint::Distance(Distance {
9396            points: vec![point0_id.into(), point1_id.into()],
9397            distance: Number {
9398                value: 2.0,
9399                units: NumericSuffix::Mm,
9400            },
9401            label_position: None,
9402            source: Default::default(),
9403        });
9404        let (src_delta, scene_delta) = frontend
9405            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9406            .await
9407            .unwrap();
9408        assert_eq!(
9409            src_delta.text.as_str(),
9410            // The lack indentation is a formatter bug.
9411            "\
9412sketch(on = XY) {
9413  point1 = point(at = [var 1, var 2])
9414  point2 = point(at = [var 3, var 4])
9415  distance([point1, point2]) == 2mm
9416}
9417"
9418        );
9419        assert_eq!(
9420            scene_delta.new_graph.objects.len(),
9421            5,
9422            "{:#?}",
9423            scene_delta.new_graph.objects
9424        );
9425
9426        ctx.close().await;
9427        mock_ctx.close().await;
9428    }
9429
9430    #[tokio::test(flavor = "multi_thread")]
9431    async fn test_distance_two_points_with_label() {
9432        let initial_source = "\
9433sketch(on = XY) {
9434  point(at = [var 1, var 2])
9435  point(at = [var 3, var 4])
9436}
9437";
9438
9439        let program = Program::parse(initial_source).unwrap().0.unwrap();
9440
9441        let mut frontend = FrontendState::new();
9442
9443        let mock_ctx = ExecutorContext::new_mock(None).await;
9444        let version = Version(0);
9445
9446        frontend.program = program.clone();
9447        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9448        frontend.update_state_after_exec(outcome, true);
9449        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9450        let sketch_id = sketch_object.id;
9451        let sketch = expect_sketch(sketch_object);
9452        let point0_id = *sketch.segments.first().unwrap();
9453        let point1_id = *sketch.segments.get(1).unwrap();
9454
9455        let label_position = Point2d {
9456            x: Number {
9457                value: 10.0,
9458                units: NumericSuffix::Mm,
9459            },
9460            y: Number {
9461                value: 11.0,
9462                units: NumericSuffix::Mm,
9463            },
9464        };
9465        let constraint = Constraint::Distance(Distance {
9466            points: vec![point0_id.into(), point1_id.into()],
9467            distance: Number {
9468                value: 2.0,
9469                units: NumericSuffix::Mm,
9470            },
9471            label_position: Some(label_position.clone()),
9472            source: Default::default(),
9473        });
9474        let (src_delta, scene_delta) = frontend
9475            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9476            .await
9477            .unwrap();
9478        assert_eq!(
9479            src_delta.text.as_str(),
9480            "\
9481sketch(on = XY) {
9482  point1 = point(at = [var 1, var 2])
9483  point2 = point(at = [var 3, var 4])
9484  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9485}
9486"
9487        );
9488
9489        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9490        let sketch = expect_sketch(sketch_object);
9491        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9492        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9493            panic!("Expected constraint object");
9494        };
9495        let Constraint::Distance(distance) = constraint else {
9496            panic!("Expected distance constraint");
9497        };
9498        assert_eq!(distance.label_position, Some(label_position));
9499
9500        mock_ctx.close().await;
9501    }
9502
9503    #[tokio::test(flavor = "multi_thread")]
9504    async fn test_edit_distance_constraint_label_position() {
9505        let initial_source = "\
9506sketch(on = XY) {
9507  point(at = [var 1, var 2])
9508  point(at = [var 3, var 2])
9509}
9510";
9511
9512        let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514        let mut frontend = FrontendState::new();
9515
9516        let mock_ctx = ExecutorContext::new_mock(None).await;
9517        let version = Version(0);
9518
9519        frontend.program = program.clone();
9520        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521        frontend.update_state_after_exec(outcome, true);
9522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523        let sketch_id = sketch_object.id;
9524        let sketch = expect_sketch(sketch_object);
9525        let point0_id = *sketch.segments.first().unwrap();
9526        let point1_id = *sketch.segments.get(1).unwrap();
9527
9528        let constraint = Constraint::Distance(Distance {
9529            points: vec![point0_id.into(), point1_id.into()],
9530            distance: Number {
9531                value: 2.0,
9532                units: NumericSuffix::Mm,
9533            },
9534            label_position: None,
9535            source: Default::default(),
9536        });
9537        let (_, scene_delta) = frontend
9538            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9539            .await
9540            .unwrap();
9541        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9542        let sketch = expect_sketch(sketch_object);
9543        let constraint_id = sketch.constraints[0];
9544        let label_position = Point2d {
9545            x: Number {
9546                value: 10.0,
9547                units: NumericSuffix::Mm,
9548            },
9549            y: Number {
9550                value: 11.0,
9551                units: NumericSuffix::Mm,
9552            },
9553        };
9554
9555        let (src_delta, scene_delta) = frontend
9556            .edit_distance_constraint_label_position(
9557                &mock_ctx,
9558                version,
9559                sketch_id,
9560                constraint_id,
9561                label_position.clone(),
9562                vec![],
9563            )
9564            .await
9565            .unwrap();
9566        assert_eq!(
9567            src_delta.text.as_str(),
9568            "\
9569sketch(on = XY) {
9570  point1 = point(at = [var 1, var 2])
9571  point2 = point(at = [var 3, var 2])
9572  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9573}
9574"
9575        );
9576
9577        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9578        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9579            panic!("Expected constraint object");
9580        };
9581        let Constraint::Distance(distance) = constraint else {
9582            panic!("Expected distance constraint");
9583        };
9584        assert_eq!(distance.label_position, Some(label_position));
9585
9586        mock_ctx.close().await;
9587    }
9588
9589    #[tokio::test(flavor = "multi_thread")]
9590    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9591        let initial_source = "\
9592sketch(on = XY) {
9593  point1 = point(at = [var 0mm, var 0mm])
9594  point2 = point(at = [var 10mm, var 0mm])
9595  distance([point1, point2]) == 5mm
9596}
9597";
9598
9599        let program = Program::parse(initial_source).unwrap().0.unwrap();
9600        let mut frontend = FrontendState::new();
9601        let mock_ctx = ExecutorContext::new_mock(None).await;
9602        let version = Version(0);
9603
9604        frontend.program = program.clone();
9605        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9606        frontend.update_state_after_exec(outcome, true);
9607        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9608        let sketch_id = sketch_object.id;
9609        let sketch = expect_sketch(sketch_object);
9610        let point0_id = sketch.segments[0];
9611        let point1_id = sketch.segments[1];
9612        let constraint_id = sketch.constraints[0];
9613
9614        let edited_segments = vec![ExistingSegmentCtor {
9615            id: point0_id,
9616            ctor: SegmentCtor::Point(PointCtor {
9617                position: Point2d {
9618                    x: Expr::Var(Number {
9619                        value: 2.0,
9620                        units: NumericSuffix::Mm,
9621                    }),
9622                    y: Expr::Var(Number {
9623                        value: 1.0,
9624                        units: NumericSuffix::Mm,
9625                    }),
9626                },
9627            }),
9628        }];
9629        let (_, scene_delta) = frontend
9630            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9631            .await
9632            .unwrap();
9633        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9634        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9635
9636        let label_position = Point2d {
9637            x: Number {
9638                value: 3.0,
9639                units: NumericSuffix::Mm,
9640            },
9641            y: Number {
9642                value: 4.0,
9643                units: NumericSuffix::Mm,
9644            },
9645        };
9646        let (_, scene_delta) = frontend
9647            .edit_distance_constraint_label_position(
9648                &mock_ctx,
9649                version,
9650                sketch_id,
9651                constraint_id,
9652                label_position,
9653                vec![point0_id],
9654            )
9655            .await
9656            .unwrap();
9657
9658        assert_point_position_close(
9659            point_position(&scene_delta.new_graph, point0_id),
9660            point0_after_segment_edit,
9661        );
9662        assert_point_position_close(
9663            point_position(&scene_delta.new_graph, point1_id),
9664            point1_after_segment_edit,
9665        );
9666
9667        mock_ctx.close().await;
9668    }
9669
9670    #[tokio::test(flavor = "multi_thread")]
9671    async fn test_edit_distance_constraint_value_commits_solved_point_guesses() {
9672        let initial_source = "\
9673sketch(on = XY) {
9674  point1 = point(at = [var 1mm, var 2mm])
9675  point2 = point(at = [var 3mm, var 2mm])
9676  distance([point1, point2]) == 2mm
9677}
9678";
9679
9680        let program = Program::parse(initial_source).unwrap().0.unwrap();
9681        let mut frontend = FrontendState::new();
9682        let mock_ctx = ExecutorContext::new_mock(None).await;
9683        let version = Version(0);
9684
9685        frontend.program = program.clone();
9686        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9687        frontend.update_state_after_exec(outcome, true);
9688        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9689        let sketch_id = sketch_object.id;
9690        let sketch = expect_sketch(sketch_object);
9691        let constraint_id = sketch.constraints[0];
9692        let point0_id = sketch.segments[0];
9693        let point1_id = sketch.segments[1];
9694
9695        let (src_delta, scene_delta) = frontend
9696            .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "4mm".to_owned())
9697            .await
9698            .unwrap();
9699
9700        assert_eq!(
9701            src_delta.text.as_str(),
9702            "\
9703sketch(on = XY) {
9704  point1 = point(at = [var 0mm, var 2mm])
9705  point2 = point(at = [var 4mm, var 2mm])
9706  distance([point1, point2]) == 4mm
9707}
9708"
9709        );
9710
9711        assert_point_position_close(
9712            point_position(&scene_delta.new_graph, point0_id),
9713            Point2d {
9714                x: Number {
9715                    value: 0.0,
9716                    units: NumericSuffix::Mm,
9717                },
9718                y: Number {
9719                    value: 2.0,
9720                    units: NumericSuffix::Mm,
9721                },
9722            },
9723        );
9724        assert_point_position_close(
9725            point_position(&scene_delta.new_graph, point1_id),
9726            Point2d {
9727                x: Number {
9728                    value: 4.0,
9729                    units: NumericSuffix::Mm,
9730                },
9731                y: Number {
9732                    value: 2.0,
9733                    units: NumericSuffix::Mm,
9734                },
9735            },
9736        );
9737
9738        mock_ctx.close().await;
9739    }
9740
9741    #[tokio::test(flavor = "multi_thread")]
9742    async fn test_distance_point_line() {
9743        let initial_source = "\
9744sketch(on = XY) {
9745  point(at = [var 0, var 5])
9746  line(start = [var 0, var 0], end = [var 10, var 0])
9747}
9748";
9749
9750        let program = Program::parse(initial_source).unwrap().0.unwrap();
9751
9752        let mut frontend = FrontendState::new();
9753
9754        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9755        let mock_ctx = ExecutorContext::new_mock(None).await;
9756        let version = Version(0);
9757
9758        frontend.hack_set_program(&ctx, program).await.unwrap();
9759        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9760        let sketch_id = sketch_object.id;
9761        let sketch = expect_sketch(sketch_object);
9762        let point_id = *sketch.segments.first().unwrap();
9763        let line_id = *sketch
9764            .segments
9765            .iter()
9766            .find(|segment_id| {
9767                matches!(
9768                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9769                    Some(ObjectKind::Segment {
9770                        segment: Segment::Line(_)
9771                    })
9772                )
9773            })
9774            .unwrap();
9775
9776        let label_position = Point2d {
9777            x: Number {
9778                value: 10.0,
9779                units: NumericSuffix::Mm,
9780            },
9781            y: Number {
9782                value: 11.0,
9783                units: NumericSuffix::Mm,
9784            },
9785        };
9786        let constraint = Constraint::Distance(Distance {
9787            points: vec![point_id.into(), line_id.into()],
9788            distance: Number {
9789                value: 5.0,
9790                units: NumericSuffix::Mm,
9791            },
9792            label_position: Some(label_position.clone()),
9793            source: Default::default(),
9794        });
9795        let (src_delta, scene_delta) = frontend
9796            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9797            .await
9798            .unwrap();
9799        assert_eq!(
9800            src_delta.text.as_str(),
9801            "\
9802sketch(on = XY) {
9803  point1 = point(at = [var 0, var 5])
9804  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9805  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9806}
9807"
9808        );
9809        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9810        let sketch = expect_sketch(sketch_object);
9811        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9812        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9813            panic!("Expected constraint object");
9814        };
9815        let Constraint::Distance(distance) = constraint else {
9816            panic!("Expected distance constraint");
9817        };
9818        assert_eq!(distance.label_position, Some(label_position));
9819
9820        ctx.close().await;
9821        mock_ctx.close().await;
9822    }
9823
9824    #[tokio::test(flavor = "multi_thread")]
9825    async fn test_distance_point_arc() {
9826        let initial_source = "\
9827sketch(on = XY) {
9828  point(at = [var 0, var 8])
9829  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9830}
9831";
9832
9833        let program = Program::parse(initial_source).unwrap().0.unwrap();
9834
9835        let mut frontend = FrontendState::new();
9836
9837        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9838        let mock_ctx = ExecutorContext::new_mock(None).await;
9839        let version = Version(0);
9840
9841        frontend.hack_set_program(&ctx, program).await.unwrap();
9842        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9843        let sketch_id = sketch_object.id;
9844        let sketch = expect_sketch(sketch_object);
9845        let point_id = *sketch.segments.first().unwrap();
9846        let arc_id = *sketch
9847            .segments
9848            .iter()
9849            .find(|segment_id| {
9850                matches!(
9851                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9852                    Some(ObjectKind::Segment {
9853                        segment: Segment::Arc(_)
9854                    })
9855                )
9856            })
9857            .unwrap();
9858
9859        let constraint = Constraint::Distance(Distance {
9860            points: vec![point_id.into(), arc_id.into()],
9861            distance: Number {
9862                value: 3.0,
9863                units: NumericSuffix::Mm,
9864            },
9865            label_position: None,
9866            source: Default::default(),
9867        });
9868        let (src_delta, _scene_delta) = frontend
9869            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9870            .await
9871            .unwrap();
9872        assert_eq!(
9873            src_delta.text.as_str(),
9874            "\
9875sketch(on = XY) {
9876  point1 = point(at = [var 0, var 8])
9877  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9878  distance([point1, arc1]) == 3mm
9879}
9880"
9881        );
9882
9883        ctx.close().await;
9884        mock_ctx.close().await;
9885    }
9886
9887    #[tokio::test(flavor = "multi_thread")]
9888    async fn test_distance_arc_origin() {
9889        let initial_source = "\
9890sketch001 = sketch(on = XY) {
9891  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9892}
9893";
9894
9895        let program = Program::parse(initial_source).unwrap().0.unwrap();
9896
9897        let mut frontend = FrontendState::new();
9898
9899        let mock_ctx = ExecutorContext::new_mock(None).await;
9900        let version = Version(0);
9901
9902        frontend.program = program.clone();
9903        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9904        frontend.update_state_after_exec(outcome, true);
9905        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9906        let sketch_id = sketch_object.id;
9907        let sketch = expect_sketch(sketch_object);
9908        let arc_id = *sketch
9909            .segments
9910            .iter()
9911            .find(|segment_id| {
9912                matches!(
9913                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9914                    Some(ObjectKind::Segment {
9915                        segment: Segment::Arc(_)
9916                    })
9917                )
9918            })
9919            .unwrap();
9920
9921        let constraint = Constraint::Distance(Distance {
9922            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
9923            distance: Number {
9924                value: 3.0,
9925                units: NumericSuffix::Mm,
9926            },
9927            label_position: None,
9928            source: Default::default(),
9929        });
9930        let (src_delta, _scene_delta) = frontend
9931            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9932            .await
9933            .unwrap();
9934        assert_eq!(
9935            src_delta.text.as_str(),
9936            "\
9937sketch001 = sketch(on = XY) {
9938  arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9939  distance([arc1, ORIGIN]) == 3mm
9940}
9941"
9942        );
9943
9944        mock_ctx.close().await;
9945    }
9946
9947    #[tokio::test(flavor = "multi_thread")]
9948    async fn test_distance_line_origin() {
9949        let initial_source = "\
9950sketch(on = XY) {
9951  line(start = [var 5, var 0], end = [var 5, var 10])
9952}
9953";
9954
9955        let program = Program::parse(initial_source).unwrap().0.unwrap();
9956
9957        let mut frontend = FrontendState::new();
9958
9959        let mock_ctx = ExecutorContext::new_mock(None).await;
9960        let version = Version(0);
9961
9962        frontend.program = program.clone();
9963        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9964        frontend.update_state_after_exec(outcome, true);
9965        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9966        let sketch_id = sketch_object.id;
9967        let sketch = expect_sketch(sketch_object);
9968        let line_id = *sketch
9969            .segments
9970            .iter()
9971            .find(|segment_id| {
9972                matches!(
9973                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9974                    Some(ObjectKind::Segment {
9975                        segment: Segment::Line(_)
9976                    })
9977                )
9978            })
9979            .unwrap();
9980
9981        let constraint = Constraint::Distance(Distance {
9982            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
9983            distance: Number {
9984                value: 5.0,
9985                units: NumericSuffix::Mm,
9986            },
9987            label_position: None,
9988            source: Default::default(),
9989        });
9990        let (src_delta, _scene_delta) = frontend
9991            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9992            .await
9993            .unwrap();
9994        assert_eq!(
9995            src_delta.text.as_str(),
9996            "\
9997sketch(on = XY) {
9998  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
9999  distance([ORIGIN, line1]) == 5mm
10000}
10001"
10002        );
10003
10004        mock_ctx.close().await;
10005    }
10006
10007    #[tokio::test(flavor = "multi_thread")]
10008    async fn test_distance_line_circle() {
10009        let initial_source = "\
10010sketch(on = XY) {
10011  line(start = [var -10, var 8], end = [var 10, var 8])
10012  circle(start = [var 5, var 0], center = [var 0, var 0])
10013}
10014";
10015
10016        let program = Program::parse(initial_source).unwrap().0.unwrap();
10017
10018        let mut frontend = FrontendState::new();
10019
10020        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10021        let mock_ctx = ExecutorContext::new_mock(None).await;
10022        let version = Version(0);
10023
10024        frontend.hack_set_program(&ctx, program).await.unwrap();
10025        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10026        let sketch_id = sketch_object.id;
10027        let sketch = expect_sketch(sketch_object);
10028        let line_id = *sketch
10029            .segments
10030            .iter()
10031            .find(|segment_id| {
10032                matches!(
10033                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10034                    Some(ObjectKind::Segment {
10035                        segment: Segment::Line(_)
10036                    })
10037                )
10038            })
10039            .unwrap();
10040        let circle_id = *sketch
10041            .segments
10042            .iter()
10043            .find(|segment_id| {
10044                matches!(
10045                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10046                    Some(ObjectKind::Segment {
10047                        segment: Segment::Circle(_)
10048                    })
10049                )
10050            })
10051            .unwrap();
10052
10053        let constraint = Constraint::Distance(Distance {
10054            points: vec![line_id.into(), circle_id.into()],
10055            distance: Number {
10056                value: 3.0,
10057                units: NumericSuffix::Mm,
10058            },
10059            label_position: None,
10060            source: Default::default(),
10061        });
10062        let (src_delta, _scene_delta) = frontend
10063            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10064            .await
10065            .unwrap();
10066        assert_eq!(
10067            src_delta.text.as_str(),
10068            "\
10069sketch(on = XY) {
10070  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
10071  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10072  distance([line1, circle1]) == 3mm
10073}
10074"
10075        );
10076
10077        ctx.close().await;
10078        mock_ctx.close().await;
10079    }
10080
10081    #[tokio::test(flavor = "multi_thread")]
10082    async fn test_distance_circle_arc() {
10083        let initial_source = "\
10084sketch(on = XY) {
10085  circle(start = [var 5, var 0], center = [var 0, var 0])
10086  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10087}
10088";
10089
10090        let program = Program::parse(initial_source).unwrap().0.unwrap();
10091
10092        let mut frontend = FrontendState::new();
10093
10094        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10095        let mock_ctx = ExecutorContext::new_mock(None).await;
10096        let version = Version(0);
10097
10098        frontend.hack_set_program(&ctx, program).await.unwrap();
10099        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10100        let sketch_id = sketch_object.id;
10101        let sketch = expect_sketch(sketch_object);
10102        let circle_id = *sketch
10103            .segments
10104            .iter()
10105            .find(|segment_id| {
10106                matches!(
10107                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10108                    Some(ObjectKind::Segment {
10109                        segment: Segment::Circle(_)
10110                    })
10111                )
10112            })
10113            .unwrap();
10114        let arc_id = *sketch
10115            .segments
10116            .iter()
10117            .find(|segment_id| {
10118                matches!(
10119                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10120                    Some(ObjectKind::Segment {
10121                        segment: Segment::Arc(_)
10122                    })
10123                )
10124            })
10125            .unwrap();
10126
10127        let constraint = Constraint::Distance(Distance {
10128            points: vec![circle_id.into(), arc_id.into()],
10129            distance: Number {
10130                value: 3.0,
10131                units: NumericSuffix::Mm,
10132            },
10133            label_position: None,
10134            source: Default::default(),
10135        });
10136        let (src_delta, _scene_delta) = frontend
10137            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10138            .await
10139            .unwrap();
10140        assert_eq!(
10141            src_delta.text.as_str(),
10142            "\
10143sketch(on = XY) {
10144  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10145  arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10146  distance([circle1, arc1]) == 3mm
10147}
10148"
10149        );
10150
10151        ctx.close().await;
10152        mock_ctx.close().await;
10153    }
10154
10155    #[tokio::test(flavor = "multi_thread")]
10156    async fn test_distance_parallel_lines() {
10157        let initial_source = "\
10158sketch(on = XY) {
10159  line(start = [var 0, var 0], end = [var 10, var 0])
10160  line(start = [var 0, var 5], end = [var 10, var 5])
10161}
10162";
10163
10164        let program = Program::parse(initial_source).unwrap().0.unwrap();
10165
10166        let mut frontend = FrontendState::new();
10167
10168        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10169        let mock_ctx = ExecutorContext::new_mock(None).await;
10170        let version = Version(0);
10171
10172        frontend.hack_set_program(&ctx, program).await.unwrap();
10173        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10174        let sketch_id = sketch_object.id;
10175        let sketch = expect_sketch(sketch_object);
10176        let line_ids = sketch
10177            .segments
10178            .iter()
10179            .copied()
10180            .filter(|segment_id| {
10181                matches!(
10182                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10183                    Some(ObjectKind::Segment {
10184                        segment: Segment::Line(_)
10185                    })
10186                )
10187            })
10188            .collect::<Vec<_>>();
10189
10190        let constraint = Constraint::Distance(Distance {
10191            points: vec![line_ids[0].into(), line_ids[1].into()],
10192            distance: Number {
10193                value: 5.0,
10194                units: NumericSuffix::Mm,
10195            },
10196            label_position: None,
10197            source: Default::default(),
10198        });
10199        let (src_delta, _scene_delta) = frontend
10200            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10201            .await
10202            .unwrap();
10203        assert_eq!(
10204            src_delta.text.as_str(),
10205            "\
10206sketch(on = XY) {
10207  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10208  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
10209  distance([line1, line2]) == 5mm
10210}
10211"
10212        );
10213
10214        ctx.close().await;
10215        mock_ctx.close().await;
10216    }
10217
10218    #[tokio::test(flavor = "multi_thread")]
10219    async fn test_distance_non_parallel_lines_lowers_to_distance() {
10220        let initial_source = "\
10221sketch(on = XY) {
10222  line(start = [var 0, var 0], end = [var 10, var 0])
10223  line(start = [var 0, var 0], end = [var 0, var 10])
10224}
10225";
10226
10227        let program = Program::parse(initial_source).unwrap().0.unwrap();
10228
10229        let mut frontend = FrontendState::new();
10230
10231        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10232        let mock_ctx = ExecutorContext::new_mock(None).await;
10233        let version = Version(0);
10234
10235        frontend.hack_set_program(&ctx, program).await.unwrap();
10236        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10237        let sketch_id = sketch_object.id;
10238        let sketch = expect_sketch(sketch_object);
10239        let line_ids = sketch
10240            .segments
10241            .iter()
10242            .copied()
10243            .filter(|segment_id| {
10244                matches!(
10245                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10246                    Some(ObjectKind::Segment {
10247                        segment: Segment::Line(_)
10248                    })
10249                )
10250            })
10251            .collect::<Vec<_>>();
10252
10253        let constraint = Constraint::Distance(Distance {
10254            points: vec![line_ids[0].into(), line_ids[1].into()],
10255            distance: Number {
10256                value: 5.0,
10257                units: NumericSuffix::Mm,
10258            },
10259            label_position: None,
10260            source: Default::default(),
10261        });
10262        let (src_delta, _scene_delta) = frontend
10263            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10264            .await
10265            .unwrap();
10266        assert_eq!(
10267            src_delta.text.as_str(),
10268            "\
10269sketch(on = XY) {
10270  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10271  line2 = line(start = [var 0, var 0], end = [var 0, var 10])
10272  distance([line1, line2]) == 5mm
10273}
10274"
10275        );
10276
10277        ctx.close().await;
10278        mock_ctx.close().await;
10279    }
10280
10281    #[tokio::test(flavor = "multi_thread")]
10282    async fn test_horizontal_distance_two_points() {
10283        let initial_source = "\
10284sketch(on = XY) {
10285  point(at = [var 1, var 2])
10286  point(at = [var 3, var 4])
10287}
10288";
10289
10290        let program = Program::parse(initial_source).unwrap().0.unwrap();
10291
10292        let mut frontend = FrontendState::new();
10293
10294        let mock_ctx = ExecutorContext::new_mock(None).await;
10295        let version = Version(0);
10296
10297        frontend.program = program.clone();
10298        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10299        frontend.update_state_after_exec(outcome, true);
10300        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10301        let sketch_id = sketch_object.id;
10302        let sketch = expect_sketch(sketch_object);
10303        let point0_id = *sketch.segments.first().unwrap();
10304        let point1_id = *sketch.segments.get(1).unwrap();
10305        let label_position = Point2d {
10306            x: Number {
10307                value: 10.0,
10308                units: NumericSuffix::Mm,
10309            },
10310            y: Number {
10311                value: 11.0,
10312                units: NumericSuffix::Mm,
10313            },
10314        };
10315
10316        let constraint = Constraint::HorizontalDistance(Distance {
10317            points: vec![point0_id.into(), point1_id.into()],
10318            distance: Number {
10319                value: 2.0,
10320                units: NumericSuffix::Mm,
10321            },
10322            label_position: Some(label_position.clone()),
10323            source: Default::default(),
10324        });
10325        let (src_delta, scene_delta) = frontend
10326            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10327            .await
10328            .unwrap();
10329        assert_eq!(
10330            src_delta.text.as_str(),
10331            // The lack indentation is a formatter bug.
10332            "\
10333sketch(on = XY) {
10334  point1 = point(at = [var 1, var 2])
10335  point2 = point(at = [var 3, var 4])
10336  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10337}
10338"
10339        );
10340        assert_eq!(
10341            scene_delta.new_graph.objects.len(),
10342            5,
10343            "{:#?}",
10344            scene_delta.new_graph.objects
10345        );
10346        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10347        let sketch = expect_sketch(sketch_object);
10348        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10349        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10350            panic!("Expected constraint object");
10351        };
10352        let Constraint::HorizontalDistance(distance) = constraint else {
10353            panic!("Expected horizontal distance constraint");
10354        };
10355        assert_eq!(distance.label_position, Some(label_position));
10356
10357        mock_ctx.close().await;
10358    }
10359
10360    #[tokio::test(flavor = "multi_thread")]
10361    async fn test_radius_single_arc_segment() {
10362        let initial_source = "\
10363sketch(on = XY) {
10364  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10365}
10366";
10367
10368        let program = Program::parse(initial_source).unwrap().0.unwrap();
10369
10370        let mut frontend = FrontendState::new();
10371
10372        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10373        let mock_ctx = ExecutorContext::new_mock(None).await;
10374        let version = Version(0);
10375
10376        frontend.hack_set_program(&ctx, program).await.unwrap();
10377        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10378        let sketch_id = sketch_object.id;
10379        let sketch = expect_sketch(sketch_object);
10380        // Find the arc segment (not the points)
10381        let arc_id = sketch
10382            .segments
10383            .iter()
10384            .find(|&seg_id| {
10385                let obj = frontend.scene_graph.objects.get(seg_id.0);
10386                matches!(
10387                    obj.map(|o| &o.kind),
10388                    Some(ObjectKind::Segment {
10389                        segment: Segment::Arc(_)
10390                    })
10391                )
10392            })
10393            .unwrap();
10394
10395        let constraint = Constraint::Radius(Radius {
10396            arc: *arc_id,
10397            radius: Number {
10398                value: 5.0,
10399                units: NumericSuffix::Mm,
10400            },
10401            label_position: None,
10402            source: Default::default(),
10403        });
10404        let (src_delta, scene_delta) = frontend
10405            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10406            .await
10407            .unwrap();
10408        assert_eq!(
10409            src_delta.text.as_str(),
10410            // The lack indentation is a formatter bug.
10411            "\
10412sketch(on = XY) {
10413  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10414  radius(arc1) == 5mm
10415}
10416"
10417        );
10418        assert_eq!(
10419            scene_delta.new_graph.objects.len(),
10420            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10421            "{:#?}",
10422            scene_delta.new_graph.objects
10423        );
10424
10425        ctx.close().await;
10426        mock_ctx.close().await;
10427    }
10428
10429    #[tokio::test(flavor = "multi_thread")]
10430    async fn test_radius_single_arc_segment_with_label_position() {
10431        let initial_source = "\
10432sketch(on = XY) {
10433  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10434}
10435";
10436
10437        let program = Program::parse(initial_source).unwrap().0.unwrap();
10438        let mut frontend = FrontendState::new();
10439        let mock_ctx = ExecutorContext::new_mock(None).await;
10440        let version = Version(0);
10441
10442        frontend.program = program.clone();
10443        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10444        frontend.update_state_after_exec(outcome, true);
10445        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10446        let sketch_id = sketch_object.id;
10447        let sketch = expect_sketch(sketch_object);
10448        let arc_id = sketch
10449            .segments
10450            .iter()
10451            .find(|&seg_id| {
10452                let obj = frontend.scene_graph.objects.get(seg_id.0);
10453                matches!(
10454                    obj.map(|o| &o.kind),
10455                    Some(ObjectKind::Segment {
10456                        segment: Segment::Arc(_)
10457                    })
10458                )
10459            })
10460            .unwrap();
10461
10462        let label_position = Point2d {
10463            x: Number {
10464                value: 10.0,
10465                units: NumericSuffix::Mm,
10466            },
10467            y: Number {
10468                value: 11.0,
10469                units: NumericSuffix::Mm,
10470            },
10471        };
10472        let constraint = Constraint::Radius(Radius {
10473            arc: *arc_id,
10474            radius: Number {
10475                value: 5.0,
10476                units: NumericSuffix::Mm,
10477            },
10478            label_position: Some(label_position.clone()),
10479            source: Default::default(),
10480        });
10481        let (src_delta, scene_delta) = frontend
10482            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10483            .await
10484            .unwrap();
10485        assert_eq!(
10486            src_delta.text.as_str(),
10487            "\
10488sketch(on = XY) {
10489  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10490  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10491}
10492"
10493        );
10494
10495        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10496        let sketch = expect_sketch(sketch_object);
10497        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10498        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10499            panic!("Expected constraint object");
10500        };
10501        let Constraint::Radius(radius) = constraint else {
10502            panic!("Expected radius constraint");
10503        };
10504        assert_eq!(radius.label_position, Some(label_position));
10505
10506        mock_ctx.close().await;
10507    }
10508
10509    #[tokio::test(flavor = "multi_thread")]
10510    async fn test_edit_radius_constraint_label_position() {
10511        let initial_source = "\
10512sketch(on = XY) {
10513  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10514  radius(arc1) == 5mm
10515}
10516";
10517
10518        let program = Program::parse(initial_source).unwrap().0.unwrap();
10519        let mut frontend = FrontendState::new();
10520        let mock_ctx = ExecutorContext::new_mock(None).await;
10521        let version = Version(0);
10522
10523        frontend.program = program.clone();
10524        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10525        frontend.update_state_after_exec(outcome, true);
10526        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10527        let sketch_id = sketch_object.id;
10528        let sketch = expect_sketch(sketch_object);
10529        let constraint_id = sketch.constraints[0];
10530        let label_position = Point2d {
10531            x: Number {
10532                value: 10.0,
10533                units: NumericSuffix::Mm,
10534            },
10535            y: Number {
10536                value: 11.0,
10537                units: NumericSuffix::Mm,
10538            },
10539        };
10540
10541        let (src_delta, scene_delta) = frontend
10542            .edit_distance_constraint_label_position(
10543                &mock_ctx,
10544                version,
10545                sketch_id,
10546                constraint_id,
10547                label_position.clone(),
10548                vec![],
10549            )
10550            .await
10551            .unwrap();
10552        assert_eq!(
10553            src_delta.text.as_str(),
10554            "\
10555sketch(on = XY) {
10556  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10557  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10558}
10559"
10560        );
10561
10562        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10563        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10564            panic!("Expected constraint object");
10565        };
10566        let Constraint::Radius(radius) = constraint else {
10567            panic!("Expected radius constraint");
10568        };
10569        assert_eq!(radius.label_position, Some(label_position));
10570
10571        mock_ctx.close().await;
10572    }
10573
10574    #[tokio::test(flavor = "multi_thread")]
10575    async fn test_vertical_distance_two_points() {
10576        let initial_source = "\
10577sketch(on = XY) {
10578  point(at = [var 1, var 2])
10579  point(at = [var 3, var 4])
10580}
10581";
10582
10583        let program = Program::parse(initial_source).unwrap().0.unwrap();
10584
10585        let mut frontend = FrontendState::new();
10586
10587        let mock_ctx = ExecutorContext::new_mock(None).await;
10588        let version = Version(0);
10589
10590        frontend.program = program.clone();
10591        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10592        frontend.update_state_after_exec(outcome, true);
10593        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10594        let sketch_id = sketch_object.id;
10595        let sketch = expect_sketch(sketch_object);
10596        let point0_id = *sketch.segments.first().unwrap();
10597        let point1_id = *sketch.segments.get(1).unwrap();
10598        let label_position = Point2d {
10599            x: Number {
10600                value: 10.0,
10601                units: NumericSuffix::Mm,
10602            },
10603            y: Number {
10604                value: 11.0,
10605                units: NumericSuffix::Mm,
10606            },
10607        };
10608
10609        let constraint = Constraint::VerticalDistance(Distance {
10610            points: vec![point0_id.into(), point1_id.into()],
10611            distance: Number {
10612                value: 2.0,
10613                units: NumericSuffix::Mm,
10614            },
10615            label_position: Some(label_position.clone()),
10616            source: Default::default(),
10617        });
10618        let (src_delta, scene_delta) = frontend
10619            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10620            .await
10621            .unwrap();
10622        assert_eq!(
10623            src_delta.text.as_str(),
10624            // The lack indentation is a formatter bug.
10625            "\
10626sketch(on = XY) {
10627  point1 = point(at = [var 1, var 2])
10628  point2 = point(at = [var 3, var 4])
10629  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10630}
10631"
10632        );
10633        assert_eq!(
10634            scene_delta.new_graph.objects.len(),
10635            5,
10636            "{:#?}",
10637            scene_delta.new_graph.objects
10638        );
10639        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10640        let sketch = expect_sketch(sketch_object);
10641        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10642        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10643            panic!("Expected constraint object");
10644        };
10645        let Constraint::VerticalDistance(distance) = constraint else {
10646            panic!("Expected vertical distance constraint");
10647        };
10648        assert_eq!(distance.label_position, Some(label_position));
10649
10650        mock_ctx.close().await;
10651    }
10652
10653    #[tokio::test(flavor = "multi_thread")]
10654    async fn test_add_fixed_standalone_point() {
10655        let initial_source = "\
10656sketch(on = XY) {
10657  point(at = [var 1, var 2])
10658}
10659";
10660
10661        let program = Program::parse(initial_source).unwrap().0.unwrap();
10662
10663        let mut frontend = FrontendState::new();
10664
10665        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10666        let mock_ctx = ExecutorContext::new_mock(None).await;
10667        let version = Version(0);
10668
10669        frontend.hack_set_program(&ctx, program).await.unwrap();
10670        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10671        let sketch_id = sketch_object.id;
10672        let sketch = expect_sketch(sketch_object);
10673        let point_id = *sketch.segments.first().unwrap();
10674
10675        let (src_delta, scene_delta) = frontend
10676            .add_constraint(
10677                &mock_ctx,
10678                version,
10679                sketch_id,
10680                Constraint::Fixed(Fixed {
10681                    points: vec![FixedPoint {
10682                        point: point_id,
10683                        position: Point2d {
10684                            x: Number {
10685                                value: 2.0,
10686                                units: NumericSuffix::Mm,
10687                            },
10688                            y: Number {
10689                                value: 3.0,
10690                                units: NumericSuffix::Mm,
10691                            },
10692                        },
10693                    }],
10694                }),
10695            )
10696            .await
10697            .unwrap();
10698        assert_eq!(
10699            src_delta.text.as_str(),
10700            "\
10701sketch(on = XY) {
10702  point1 = point(at = [var 1, var 2])
10703  fixed([point1, [2mm, 3mm]])
10704}
10705"
10706        );
10707        assert_eq!(
10708            scene_delta.new_graph.objects.len(),
10709            4,
10710            "{:#?}",
10711            scene_delta.new_graph.objects
10712        );
10713
10714        ctx.close().await;
10715        mock_ctx.close().await;
10716    }
10717
10718    #[tokio::test(flavor = "multi_thread")]
10719    async fn test_add_fixed_multiple_points() {
10720        let initial_source = "\
10721sketch(on = XY) {
10722  point(at = [var 1, var 2])
10723  point(at = [var 3, var 4])
10724}
10725";
10726
10727        let program = Program::parse(initial_source).unwrap().0.unwrap();
10728
10729        let mut frontend = FrontendState::new();
10730
10731        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10732        let mock_ctx = ExecutorContext::new_mock(None).await;
10733        let version = Version(0);
10734
10735        frontend.hack_set_program(&ctx, program).await.unwrap();
10736        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10737        let sketch_id = sketch_object.id;
10738        let sketch = expect_sketch(sketch_object);
10739        let point0_id = *sketch.segments.first().unwrap();
10740        let point1_id = *sketch.segments.get(1).unwrap();
10741
10742        let (src_delta, scene_delta) = frontend
10743            .add_constraint(
10744                &mock_ctx,
10745                version,
10746                sketch_id,
10747                Constraint::Fixed(Fixed {
10748                    points: vec![
10749                        FixedPoint {
10750                            point: point0_id,
10751                            position: Point2d {
10752                                x: Number {
10753                                    value: 2.0,
10754                                    units: NumericSuffix::Mm,
10755                                },
10756                                y: Number {
10757                                    value: 3.0,
10758                                    units: NumericSuffix::Mm,
10759                                },
10760                            },
10761                        },
10762                        FixedPoint {
10763                            point: point1_id,
10764                            position: Point2d {
10765                                x: Number {
10766                                    value: 4.0,
10767                                    units: NumericSuffix::Mm,
10768                                },
10769                                y: Number {
10770                                    value: 5.0,
10771                                    units: NumericSuffix::Mm,
10772                                },
10773                            },
10774                        },
10775                    ],
10776                }),
10777            )
10778            .await
10779            .unwrap();
10780        assert_eq!(
10781            src_delta.text.as_str(),
10782            "\
10783sketch(on = XY) {
10784  point1 = point(at = [var 1, var 2])
10785  point2 = point(at = [var 3, var 4])
10786  fixed([point1, [2mm, 3mm]])
10787  fixed([point2, [4mm, 5mm]])
10788}
10789"
10790        );
10791        assert_eq!(
10792            scene_delta.new_graph.objects.len(),
10793            6,
10794            "{:#?}",
10795            scene_delta.new_graph.objects
10796        );
10797
10798        ctx.close().await;
10799        mock_ctx.close().await;
10800    }
10801
10802    #[tokio::test(flavor = "multi_thread")]
10803    async fn test_add_fixed_owned_point() {
10804        let initial_source = "\
10805sketch(on = XY) {
10806  line(start = [var 1, var 2], end = [var 3, var 4])
10807}
10808";
10809
10810        let program = Program::parse(initial_source).unwrap().0.unwrap();
10811
10812        let mut frontend = FrontendState::new();
10813
10814        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10815        let mock_ctx = ExecutorContext::new_mock(None).await;
10816        let version = Version(0);
10817
10818        frontend.hack_set_program(&ctx, program).await.unwrap();
10819        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10820        let sketch_id = sketch_object.id;
10821        let sketch = expect_sketch(sketch_object);
10822        let line_start_id = *sketch.segments.first().unwrap();
10823
10824        let (src_delta, scene_delta) = frontend
10825            .add_constraint(
10826                &mock_ctx,
10827                version,
10828                sketch_id,
10829                Constraint::Fixed(Fixed {
10830                    points: vec![FixedPoint {
10831                        point: line_start_id,
10832                        position: Point2d {
10833                            x: Number {
10834                                value: 2.0,
10835                                units: NumericSuffix::Mm,
10836                            },
10837                            y: Number {
10838                                value: 3.0,
10839                                units: NumericSuffix::Mm,
10840                            },
10841                        },
10842                    }],
10843                }),
10844            )
10845            .await
10846            .unwrap();
10847        assert_eq!(
10848            src_delta.text.as_str(),
10849            "\
10850sketch(on = XY) {
10851  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10852  fixed([line1.start, [2mm, 3mm]])
10853}
10854"
10855        );
10856        assert_eq!(
10857            scene_delta.new_graph.objects.len(),
10858            6,
10859            "{:#?}",
10860            scene_delta.new_graph.objects
10861        );
10862
10863        ctx.close().await;
10864        mock_ctx.close().await;
10865    }
10866
10867    #[tokio::test(flavor = "multi_thread")]
10868    async fn test_radius_error_cases() {
10869        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10870        let mock_ctx = ExecutorContext::new_mock(None).await;
10871        let version = Version(0);
10872
10873        // Test: Single point should error
10874        let initial_source_point = "\
10875sketch(on = XY) {
10876  point(at = [var 1, var 2])
10877}
10878";
10879        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10880        let mut frontend_point = FrontendState::new();
10881        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10882        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10883        let sketch_id_point = sketch_object_point.id;
10884        let sketch_point = expect_sketch(sketch_object_point);
10885        let point_id = *sketch_point.segments.first().unwrap();
10886
10887        let constraint_point = Constraint::Radius(Radius {
10888            arc: point_id,
10889            radius: Number {
10890                value: 5.0,
10891                units: NumericSuffix::Mm,
10892            },
10893            label_position: None,
10894            source: Default::default(),
10895        });
10896        let result_point = frontend_point
10897            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10898            .await;
10899        assert!(result_point.is_err(), "Single point should error for radius");
10900
10901        // Test: Single line segment should error (only arc segments supported)
10902        let initial_source_line = "\
10903sketch(on = XY) {
10904  line(start = [var 1, var 2], end = [var 3, var 4])
10905}
10906";
10907        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10908        let mut frontend_line = FrontendState::new();
10909        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10910        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10911        let sketch_id_line = sketch_object_line.id;
10912        let sketch_line = expect_sketch(sketch_object_line);
10913        let line_id = *sketch_line.segments.first().unwrap();
10914
10915        let constraint_line = Constraint::Radius(Radius {
10916            arc: line_id,
10917            radius: Number {
10918                value: 5.0,
10919                units: NumericSuffix::Mm,
10920            },
10921            label_position: None,
10922            source: Default::default(),
10923        });
10924        let result_line = frontend_line
10925            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10926            .await;
10927        assert!(result_line.is_err(), "Single line segment should error for radius");
10928
10929        ctx.close().await;
10930        mock_ctx.close().await;
10931    }
10932
10933    #[tokio::test(flavor = "multi_thread")]
10934    async fn test_diameter_single_arc_segment() {
10935        let initial_source = "\
10936sketch(on = XY) {
10937  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10938}
10939";
10940
10941        let program = Program::parse(initial_source).unwrap().0.unwrap();
10942
10943        let mut frontend = FrontendState::new();
10944
10945        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10946        let mock_ctx = ExecutorContext::new_mock(None).await;
10947        let version = Version(0);
10948
10949        frontend.hack_set_program(&ctx, program).await.unwrap();
10950        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10951        let sketch_id = sketch_object.id;
10952        let sketch = expect_sketch(sketch_object);
10953        // Find the arc segment (not the points)
10954        let arc_id = sketch
10955            .segments
10956            .iter()
10957            .find(|&seg_id| {
10958                let obj = frontend.scene_graph.objects.get(seg_id.0);
10959                matches!(
10960                    obj.map(|o| &o.kind),
10961                    Some(ObjectKind::Segment {
10962                        segment: Segment::Arc(_)
10963                    })
10964                )
10965            })
10966            .unwrap();
10967
10968        let constraint = Constraint::Diameter(Diameter {
10969            arc: *arc_id,
10970            diameter: Number {
10971                value: 10.0,
10972                units: NumericSuffix::Mm,
10973            },
10974            label_position: None,
10975            source: Default::default(),
10976        });
10977        let (src_delta, scene_delta) = frontend
10978            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10979            .await
10980            .unwrap();
10981        assert_eq!(
10982            src_delta.text.as_str(),
10983            // The lack indentation is a formatter bug.
10984            "\
10985sketch(on = XY) {
10986  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10987  diameter(arc1) == 10mm
10988}
10989"
10990        );
10991        assert_eq!(
10992            scene_delta.new_graph.objects.len(),
10993            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10994            "{:#?}",
10995            scene_delta.new_graph.objects
10996        );
10997
10998        ctx.close().await;
10999        mock_ctx.close().await;
11000    }
11001
11002    #[tokio::test(flavor = "multi_thread")]
11003    async fn test_diameter_single_arc_segment_with_label_position() {
11004        let initial_source = "\
11005sketch(on = XY) {
11006  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11007}
11008";
11009
11010        let program = Program::parse(initial_source).unwrap().0.unwrap();
11011        let mut frontend = FrontendState::new();
11012        let mock_ctx = ExecutorContext::new_mock(None).await;
11013        let version = Version(0);
11014
11015        frontend.program = program.clone();
11016        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11017        frontend.update_state_after_exec(outcome, true);
11018        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11019        let sketch_id = sketch_object.id;
11020        let sketch = expect_sketch(sketch_object);
11021        let arc_id = sketch
11022            .segments
11023            .iter()
11024            .find(|&seg_id| {
11025                let obj = frontend.scene_graph.objects.get(seg_id.0);
11026                matches!(
11027                    obj.map(|o| &o.kind),
11028                    Some(ObjectKind::Segment {
11029                        segment: Segment::Arc(_)
11030                    })
11031                )
11032            })
11033            .unwrap();
11034
11035        let label_position = Point2d {
11036            x: Number {
11037                value: 10.0,
11038                units: NumericSuffix::Mm,
11039            },
11040            y: Number {
11041                value: 11.0,
11042                units: NumericSuffix::Mm,
11043            },
11044        };
11045        let constraint = Constraint::Diameter(Diameter {
11046            arc: *arc_id,
11047            diameter: Number {
11048                value: 10.0,
11049                units: NumericSuffix::Mm,
11050            },
11051            label_position: Some(label_position.clone()),
11052            source: Default::default(),
11053        });
11054        let (src_delta, scene_delta) = frontend
11055            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11056            .await
11057            .unwrap();
11058        assert_eq!(
11059            src_delta.text.as_str(),
11060            "\
11061sketch(on = XY) {
11062  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11063  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11064}
11065"
11066        );
11067
11068        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069        let sketch = expect_sketch(sketch_object);
11070        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11071        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11072            panic!("Expected constraint object");
11073        };
11074        let Constraint::Diameter(diameter) = constraint else {
11075            panic!("Expected diameter constraint");
11076        };
11077        assert_eq!(diameter.label_position, Some(label_position));
11078
11079        mock_ctx.close().await;
11080    }
11081
11082    #[tokio::test(flavor = "multi_thread")]
11083    async fn test_edit_diameter_constraint_label_position() {
11084        let initial_source = "\
11085sketch(on = XY) {
11086  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11087  diameter(arc1) == 10mm
11088}
11089";
11090
11091        let program = Program::parse(initial_source).unwrap().0.unwrap();
11092        let mut frontend = FrontendState::new();
11093        let mock_ctx = ExecutorContext::new_mock(None).await;
11094        let version = Version(0);
11095
11096        frontend.program = program.clone();
11097        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11098        frontend.update_state_after_exec(outcome, true);
11099        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11100        let sketch_id = sketch_object.id;
11101        let sketch = expect_sketch(sketch_object);
11102        let constraint_id = sketch.constraints[0];
11103        let label_position = Point2d {
11104            x: Number {
11105                value: 10.0,
11106                units: NumericSuffix::Mm,
11107            },
11108            y: Number {
11109                value: 11.0,
11110                units: NumericSuffix::Mm,
11111            },
11112        };
11113
11114        let (src_delta, scene_delta) = frontend
11115            .edit_distance_constraint_label_position(
11116                &mock_ctx,
11117                version,
11118                sketch_id,
11119                constraint_id,
11120                label_position.clone(),
11121                vec![],
11122            )
11123            .await
11124            .unwrap();
11125        assert_eq!(
11126            src_delta.text.as_str(),
11127            "\
11128sketch(on = XY) {
11129  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11130  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11131}
11132"
11133        );
11134
11135        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11136        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11137            panic!("Expected constraint object");
11138        };
11139        let Constraint::Diameter(diameter) = constraint else {
11140            panic!("Expected diameter constraint");
11141        };
11142        assert_eq!(diameter.label_position, Some(label_position));
11143
11144        mock_ctx.close().await;
11145    }
11146
11147    #[tokio::test(flavor = "multi_thread")]
11148    async fn test_diameter_error_cases() {
11149        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11150        let mock_ctx = ExecutorContext::new_mock(None).await;
11151        let version = Version(0);
11152
11153        // Test: Single point should error
11154        let initial_source_point = "\
11155sketch(on = XY) {
11156  point(at = [var 1, var 2])
11157}
11158";
11159        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11160        let mut frontend_point = FrontendState::new();
11161        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11162        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11163        let sketch_id_point = sketch_object_point.id;
11164        let sketch_point = expect_sketch(sketch_object_point);
11165        let point_id = *sketch_point.segments.first().unwrap();
11166
11167        let constraint_point = Constraint::Diameter(Diameter {
11168            arc: point_id,
11169            diameter: Number {
11170                value: 10.0,
11171                units: NumericSuffix::Mm,
11172            },
11173            label_position: None,
11174            source: Default::default(),
11175        });
11176        let result_point = frontend_point
11177            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11178            .await;
11179        assert!(result_point.is_err(), "Single point should error for diameter");
11180
11181        // Test: Single line segment should error (only arc segments supported)
11182        let initial_source_line = "\
11183sketch(on = XY) {
11184  line(start = [var 1, var 2], end = [var 3, var 4])
11185}
11186";
11187        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11188        let mut frontend_line = FrontendState::new();
11189        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11190        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11191        let sketch_id_line = sketch_object_line.id;
11192        let sketch_line = expect_sketch(sketch_object_line);
11193        let line_id = *sketch_line.segments.first().unwrap();
11194
11195        let constraint_line = Constraint::Diameter(Diameter {
11196            arc: line_id,
11197            diameter: Number {
11198                value: 10.0,
11199                units: NumericSuffix::Mm,
11200            },
11201            label_position: None,
11202            source: Default::default(),
11203        });
11204        let result_line = frontend_line
11205            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11206            .await;
11207        assert!(result_line.is_err(), "Single line segment should error for diameter");
11208
11209        ctx.close().await;
11210        mock_ctx.close().await;
11211    }
11212
11213    #[tokio::test(flavor = "multi_thread")]
11214    async fn test_line_horizontal() {
11215        let initial_source = "\
11216sketch(on = XY) {
11217  line(start = [var 1, var 2], end = [var 3, var 4])
11218}
11219";
11220
11221        let program = Program::parse(initial_source).unwrap().0.unwrap();
11222
11223        let mut frontend = FrontendState::new();
11224
11225        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11226        let mock_ctx = ExecutorContext::new_mock(None).await;
11227        let version = Version(0);
11228
11229        frontend.hack_set_program(&ctx, program).await.unwrap();
11230        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11231        let sketch_id = sketch_object.id;
11232        let sketch = expect_sketch(sketch_object);
11233        let line1_id = *sketch.segments.get(2).unwrap();
11234
11235        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
11236        let (src_delta, scene_delta) = frontend
11237            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11238            .await
11239            .unwrap();
11240        assert_eq!(
11241            src_delta.text.as_str(),
11242            "\
11243sketch(on = XY) {
11244  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11245  horizontal(line1)
11246}
11247"
11248        );
11249        assert_eq!(
11250            scene_delta.new_graph.objects.len(),
11251            6,
11252            "{:#?}",
11253            scene_delta.new_graph.objects
11254        );
11255
11256        ctx.close().await;
11257        mock_ctx.close().await;
11258    }
11259
11260    #[tokio::test(flavor = "multi_thread")]
11261    async fn test_line_vertical() {
11262        let initial_source = "\
11263sketch(on = XY) {
11264  line(start = [var 1, var 2], end = [var 3, var 4])
11265}
11266";
11267
11268        let program = Program::parse(initial_source).unwrap().0.unwrap();
11269
11270        let mut frontend = FrontendState::new();
11271
11272        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11273        let mock_ctx = ExecutorContext::new_mock(None).await;
11274        let version = Version(0);
11275
11276        frontend.hack_set_program(&ctx, program).await.unwrap();
11277        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11278        let sketch_id = sketch_object.id;
11279        let sketch = expect_sketch(sketch_object);
11280        let line1_id = *sketch.segments.get(2).unwrap();
11281
11282        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
11283        let (src_delta, scene_delta) = frontend
11284            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11285            .await
11286            .unwrap();
11287        assert_eq!(
11288            src_delta.text.as_str(),
11289            "\
11290sketch(on = XY) {
11291  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11292  vertical(line1)
11293}
11294"
11295        );
11296        assert_eq!(
11297            scene_delta.new_graph.objects.len(),
11298            6,
11299            "{:#?}",
11300            scene_delta.new_graph.objects
11301        );
11302
11303        ctx.close().await;
11304        mock_ctx.close().await;
11305    }
11306
11307    #[tokio::test(flavor = "multi_thread")]
11308    async fn test_points_vertical() {
11309        let initial_source = "\
11310sketch001 = sketch(on = XY) {
11311  p0 = point(at = [var -2.23mm, var 3.1mm])
11312  pf = point(at = [4, 4])
11313}
11314";
11315
11316        let program = Program::parse(initial_source).unwrap().0.unwrap();
11317
11318        let mut frontend = FrontendState::new();
11319
11320        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11321        let mock_ctx = ExecutorContext::new_mock(None).await;
11322        let version = Version(0);
11323
11324        frontend.hack_set_program(&ctx, program).await.unwrap();
11325        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11326        let sketch_id = sketch_object.id;
11327        let sketch = expect_sketch(sketch_object);
11328        let point_ids = vec![
11329            sketch.segments.first().unwrap().to_owned(),
11330            sketch.segments.get(1).unwrap().to_owned(),
11331        ];
11332
11333        let constraint = Constraint::Vertical(Vertical::Points {
11334            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11335        });
11336        let (src_delta, scene_delta) = frontend
11337            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11338            .await
11339            .unwrap();
11340        assert_eq!(
11341            src_delta.text.as_str(),
11342            "\
11343sketch001 = sketch(on = XY) {
11344  p0 = point(at = [var -2.23mm, var 3.1mm])
11345  pf = point(at = [4, 4])
11346  vertical([p0, pf])
11347}
11348"
11349        );
11350        assert_eq!(
11351            scene_delta.new_graph.objects.len(),
11352            5,
11353            "{:#?}",
11354            scene_delta.new_graph.objects
11355        );
11356
11357        ctx.close().await;
11358        mock_ctx.close().await;
11359    }
11360
11361    #[tokio::test(flavor = "multi_thread")]
11362    async fn test_points_horizontal() {
11363        let initial_source = "\
11364sketch001 = sketch(on = XY) {
11365  p0 = point(at = [var -2.23mm, var 3.1mm])
11366  pf = point(at = [4, 4])
11367}
11368";
11369
11370        let program = Program::parse(initial_source).unwrap().0.unwrap();
11371
11372        let mut frontend = FrontendState::new();
11373
11374        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11375        let mock_ctx = ExecutorContext::new_mock(None).await;
11376        let version = Version(0);
11377
11378        frontend.hack_set_program(&ctx, program).await.unwrap();
11379        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11380        let sketch_id = sketch_object.id;
11381        let sketch = expect_sketch(sketch_object);
11382        let point_ids = vec![
11383            sketch.segments.first().unwrap().to_owned(),
11384            sketch.segments.get(1).unwrap().to_owned(),
11385        ];
11386
11387        let constraint = Constraint::Horizontal(Horizontal::Points {
11388            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11389        });
11390        let (src_delta, scene_delta) = frontend
11391            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11392            .await
11393            .unwrap();
11394        assert_eq!(
11395            src_delta.text.as_str(),
11396            "\
11397sketch001 = sketch(on = XY) {
11398  p0 = point(at = [var -2.23mm, var 3.1mm])
11399  pf = point(at = [4, 4])
11400  horizontal([p0, pf])
11401}
11402"
11403        );
11404        assert_eq!(
11405            scene_delta.new_graph.objects.len(),
11406            5,
11407            "{:#?}",
11408            scene_delta.new_graph.objects
11409        );
11410
11411        ctx.close().await;
11412        mock_ctx.close().await;
11413    }
11414
11415    #[tokio::test(flavor = "multi_thread")]
11416    async fn test_point_horizontal_with_origin() {
11417        let initial_source = "\
11418sketch001 = sketch(on = XY) {
11419  p0 = point(at = [var -2.23mm, var 3.1mm])
11420}
11421";
11422
11423        let program = Program::parse(initial_source).unwrap().0.unwrap();
11424
11425        let mut frontend = FrontendState::new();
11426
11427        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11428        let mock_ctx = ExecutorContext::new_mock(None).await;
11429        let version = Version(0);
11430
11431        frontend.hack_set_program(&ctx, program).await.unwrap();
11432        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11433        let sketch_id = sketch_object.id;
11434        let sketch = expect_sketch(sketch_object);
11435        let point_id = *sketch.segments.first().unwrap();
11436
11437        let constraint = Constraint::Horizontal(Horizontal::Points {
11438            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11439        });
11440        let (src_delta, scene_delta) = frontend
11441            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11442            .await
11443            .unwrap();
11444        assert_eq!(
11445            src_delta.text.as_str(),
11446            "\
11447sketch001 = sketch(on = XY) {
11448  p0 = point(at = [var -2.23mm, var 3.1mm])
11449  horizontal([p0, ORIGIN])
11450}
11451"
11452        );
11453        assert_eq!(
11454            scene_delta.new_graph.objects.len(),
11455            4,
11456            "{:#?}",
11457            scene_delta.new_graph.objects
11458        );
11459
11460        ctx.close().await;
11461        mock_ctx.close().await;
11462    }
11463
11464    #[tokio::test(flavor = "multi_thread")]
11465    async fn test_lines_equal_length() {
11466        let initial_source = "\
11467sketch(on = XY) {
11468  line(start = [var 1, var 2], end = [var 3, var 4])
11469  line(start = [var 5, var 6], end = [var 7, var 8])
11470}
11471";
11472
11473        let program = Program::parse(initial_source).unwrap().0.unwrap();
11474
11475        let mut frontend = FrontendState::new();
11476
11477        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11478        let mock_ctx = ExecutorContext::new_mock(None).await;
11479        let version = Version(0);
11480
11481        frontend.hack_set_program(&ctx, program).await.unwrap();
11482        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11483        let sketch_id = sketch_object.id;
11484        let sketch = expect_sketch(sketch_object);
11485        let line1_id = *sketch.segments.get(2).unwrap();
11486        let line2_id = *sketch.segments.get(5).unwrap();
11487
11488        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11489            lines: vec![line1_id, line2_id],
11490        });
11491        let (src_delta, scene_delta) = frontend
11492            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11493            .await
11494            .unwrap();
11495        assert_eq!(
11496            src_delta.text.as_str(),
11497            "\
11498sketch(on = XY) {
11499  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11500  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11501  equalLength([line1, line2])
11502}
11503"
11504        );
11505        assert_eq!(
11506            scene_delta.new_graph.objects.len(),
11507            9,
11508            "{:#?}",
11509            scene_delta.new_graph.objects
11510        );
11511
11512        ctx.close().await;
11513        mock_ctx.close().await;
11514    }
11515
11516    #[tokio::test(flavor = "multi_thread")]
11517    async fn test_add_constraint_multi_line_equal_length() {
11518        let initial_source = "\
11519sketch(on = XY) {
11520  line(start = [var 1, var 2], end = [var 3, var 4])
11521  line(start = [var 5, var 6], end = [var 7, var 8])
11522  line(start = [var 9, var 10], end = [var 11, var 12])
11523}
11524";
11525
11526        let program = Program::parse(initial_source).unwrap().0.unwrap();
11527
11528        let mut frontend = FrontendState::new();
11529        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11530        let mock_ctx = ExecutorContext::new_mock(None).await;
11531        let version = Version(0);
11532
11533        frontend.hack_set_program(&ctx, program).await.unwrap();
11534        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11535        let sketch_id = sketch_object.id;
11536        let sketch = expect_sketch(sketch_object);
11537        let line1_id = *sketch.segments.get(2).unwrap();
11538        let line2_id = *sketch.segments.get(5).unwrap();
11539        let line3_id = *sketch.segments.get(8).unwrap();
11540
11541        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11542            lines: vec![line1_id, line2_id, line3_id],
11543        });
11544        let (src_delta, scene_delta) = frontend
11545            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11546            .await
11547            .unwrap();
11548        assert_eq!(
11549            src_delta.text.as_str(),
11550            "\
11551sketch(on = XY) {
11552  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11553  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11554  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11555  equalLength([line1, line2, line3])
11556}
11557"
11558        );
11559        let constraints = scene_delta
11560            .new_graph
11561            .objects
11562            .iter()
11563            .filter_map(|obj| {
11564                let ObjectKind::Constraint { constraint } = &obj.kind else {
11565                    return None;
11566                };
11567                Some(constraint)
11568            })
11569            .collect::<Vec<_>>();
11570
11571        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
11572        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
11573            panic!("expected equal length constraint, got {:?}", constraints[0]);
11574        };
11575        assert_eq!(lines_equal_length.lines.len(), 3);
11576
11577        ctx.close().await;
11578        mock_ctx.close().await;
11579    }
11580
11581    #[tokio::test(flavor = "multi_thread")]
11582    async fn test_lines_parallel() {
11583        let initial_source = "\
11584sketch(on = XY) {
11585  line(start = [var 1, var 2], end = [var 3, var 4])
11586  line(start = [var 5, var 6], end = [var 7, var 8])
11587}
11588";
11589
11590        let program = Program::parse(initial_source).unwrap().0.unwrap();
11591
11592        let mut frontend = FrontendState::new();
11593
11594        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11595        let mock_ctx = ExecutorContext::new_mock(None).await;
11596        let version = Version(0);
11597
11598        frontend.hack_set_program(&ctx, program).await.unwrap();
11599        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11600        let sketch_id = sketch_object.id;
11601        let sketch = expect_sketch(sketch_object);
11602        let line1_id = *sketch.segments.get(2).unwrap();
11603        let line2_id = *sketch.segments.get(5).unwrap();
11604
11605        let constraint = Constraint::Parallel(Parallel {
11606            lines: vec![line1_id, line2_id],
11607        });
11608        let (src_delta, scene_delta) = frontend
11609            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11610            .await
11611            .unwrap();
11612        assert_eq!(
11613            src_delta.text.as_str(),
11614            "\
11615sketch(on = XY) {
11616  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11617  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11618  parallel([line1, line2])
11619}
11620"
11621        );
11622        assert_eq!(
11623            scene_delta.new_graph.objects.len(),
11624            9,
11625            "{:#?}",
11626            scene_delta.new_graph.objects
11627        );
11628
11629        ctx.close().await;
11630        mock_ctx.close().await;
11631    }
11632
11633    #[tokio::test(flavor = "multi_thread")]
11634    async fn test_lines_parallel_multiline() {
11635        let initial_source = "\
11636sketch(on = XY) {
11637  line(start = [var 1, var 2], end = [var 3, var 4])
11638  line(start = [var 5, var 6], end = [var 7, var 8])
11639  line(start = [var 9, var 10], end = [var 11, var 12])
11640}
11641";
11642
11643        let program = Program::parse(initial_source).unwrap().0.unwrap();
11644
11645        let mut frontend = FrontendState::new();
11646
11647        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11648        let mock_ctx = ExecutorContext::new_mock(None).await;
11649        let version = Version(0);
11650
11651        frontend.hack_set_program(&ctx, program).await.unwrap();
11652        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11653        let sketch_id = sketch_object.id;
11654        let sketch = expect_sketch(sketch_object);
11655        let line1_id = *sketch.segments.get(2).unwrap();
11656        let line2_id = *sketch.segments.get(5).unwrap();
11657        let line3_id = *sketch.segments.get(8).unwrap();
11658
11659        let constraint = Constraint::Parallel(Parallel {
11660            lines: vec![line1_id, line2_id, line3_id],
11661        });
11662        let (src_delta, scene_delta) = frontend
11663            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11664            .await
11665            .unwrap();
11666        assert_eq!(
11667            src_delta.text.as_str(),
11668            "\
11669sketch(on = XY) {
11670  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11671  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11672  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11673  parallel([line1, line2, line3])
11674}
11675"
11676        );
11677
11678        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11679        let sketch = expect_sketch(sketch_object);
11680        assert_eq!(sketch.constraints.len(), 1);
11681
11682        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11683        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11684            panic!("Expected constraint object");
11685        };
11686        let Constraint::Parallel(parallel) = constraint else {
11687            panic!("Expected parallel constraint");
11688        };
11689        assert_eq!(parallel.lines.len(), 3);
11690
11691        ctx.close().await;
11692        mock_ctx.close().await;
11693    }
11694
11695    #[tokio::test(flavor = "multi_thread")]
11696    async fn test_lines_perpendicular() {
11697        let initial_source = "\
11698sketch(on = XY) {
11699  line(start = [var 1, var 2], end = [var 3, var 4])
11700  line(start = [var 5, var 6], end = [var 7, var 8])
11701}
11702";
11703
11704        let program = Program::parse(initial_source).unwrap().0.unwrap();
11705
11706        let mut frontend = FrontendState::new();
11707
11708        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11709        let mock_ctx = ExecutorContext::new_mock(None).await;
11710        let version = Version(0);
11711
11712        frontend.hack_set_program(&ctx, program).await.unwrap();
11713        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11714        let sketch_id = sketch_object.id;
11715        let sketch = expect_sketch(sketch_object);
11716        let line1_id = *sketch.segments.get(2).unwrap();
11717        let line2_id = *sketch.segments.get(5).unwrap();
11718
11719        let constraint = Constraint::Perpendicular(Perpendicular {
11720            lines: vec![line1_id, line2_id],
11721        });
11722        let (src_delta, scene_delta) = frontend
11723            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11724            .await
11725            .unwrap();
11726        assert_eq!(
11727            src_delta.text.as_str(),
11728            "\
11729sketch(on = XY) {
11730  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11731  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11732  perpendicular([line1, line2])
11733}
11734"
11735        );
11736        assert_eq!(
11737            scene_delta.new_graph.objects.len(),
11738            9,
11739            "{:#?}",
11740            scene_delta.new_graph.objects
11741        );
11742
11743        ctx.close().await;
11744        mock_ctx.close().await;
11745    }
11746
11747    #[tokio::test(flavor = "multi_thread")]
11748    async fn test_lines_angle() {
11749        let initial_source = "\
11750sketch(on = XY) {
11751  line(start = [var 1, var 2], end = [var 3, var 4])
11752  line(start = [var 5, var 6], end = [var 7, var 8])
11753}
11754";
11755
11756        let program = Program::parse(initial_source).unwrap().0.unwrap();
11757
11758        let mut frontend = FrontendState::new();
11759
11760        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11761        let mock_ctx = ExecutorContext::new_mock(None).await;
11762        let version = Version(0);
11763
11764        frontend.hack_set_program(&ctx, program).await.unwrap();
11765        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11766        let sketch_id = sketch_object.id;
11767        let sketch = expect_sketch(sketch_object);
11768        let line1_id = *sketch.segments.get(2).unwrap();
11769        let line2_id = *sketch.segments.get(5).unwrap();
11770
11771        let constraint = Constraint::Angle(Angle {
11772            lines: vec![line1_id, line2_id],
11773            angle: Number {
11774                value: 30.0,
11775                units: NumericSuffix::Deg,
11776            },
11777            source: Default::default(),
11778        });
11779        let (src_delta, scene_delta) = frontend
11780            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11781            .await
11782            .unwrap();
11783        assert_eq!(
11784            src_delta.text.as_str(),
11785            // The lack indentation is a formatter bug.
11786            "\
11787sketch(on = XY) {
11788  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11789  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11790  angle([line1, line2]) == 30deg
11791}
11792"
11793        );
11794        assert_eq!(
11795            scene_delta.new_graph.objects.len(),
11796            9,
11797            "{:#?}",
11798            scene_delta.new_graph.objects
11799        );
11800
11801        ctx.close().await;
11802        mock_ctx.close().await;
11803    }
11804
11805    #[tokio::test(flavor = "multi_thread")]
11806    async fn test_segments_tangent() {
11807        let initial_source = "\
11808sketch(on = XY) {
11809  line(start = [var 1, var 2], end = [var 3, var 4])
11810  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11811}
11812";
11813
11814        let program = Program::parse(initial_source).unwrap().0.unwrap();
11815
11816        let mut frontend = FrontendState::new();
11817
11818        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11819        let mock_ctx = ExecutorContext::new_mock(None).await;
11820        let version = Version(0);
11821
11822        frontend.hack_set_program(&ctx, program).await.unwrap();
11823        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11824        let sketch_id = sketch_object.id;
11825        let sketch = expect_sketch(sketch_object);
11826        let line1_id = *sketch.segments.get(2).unwrap();
11827        let arc1_id = *sketch.segments.get(6).unwrap();
11828
11829        let constraint = Constraint::Tangent(Tangent {
11830            input: vec![line1_id, arc1_id],
11831        });
11832        let (src_delta, scene_delta) = frontend
11833            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11834            .await
11835            .unwrap();
11836        assert_eq!(
11837            src_delta.text.as_str(),
11838            "\
11839sketch(on = XY) {
11840  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11841  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11842  tangent([line1, arc1])
11843}
11844"
11845        );
11846        assert_eq!(
11847            scene_delta.new_graph.objects.len(),
11848            10,
11849            "{:#?}",
11850            scene_delta.new_graph.objects
11851        );
11852
11853        ctx.close().await;
11854        mock_ctx.close().await;
11855    }
11856
11857    #[tokio::test(flavor = "multi_thread")]
11858    async fn test_point_midpoint() {
11859        let initial_source = "\
11860sketch(on = XY) {
11861  point(at = [var 1, var 1])
11862  line(start = [var 0, var 0], end = [var 6, var 4])
11863}
11864";
11865
11866        let program = Program::parse(initial_source).unwrap().0.unwrap();
11867
11868        let mut frontend = FrontendState::new();
11869
11870        let ctx = ExecutorContext::new_mock(None).await;
11871        let version = Version(0);
11872
11873        frontend.program = program.clone();
11874        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11875        frontend.update_state_after_exec(outcome, true);
11876        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11877        let sketch_id = sketch_object.id;
11878        let sketch = expect_sketch(sketch_object);
11879        let point_id = *sketch.segments.first().unwrap();
11880        let line_id = *sketch.segments.get(3).unwrap();
11881
11882        let constraint = Constraint::Midpoint(Midpoint {
11883            point: point_id,
11884            segment: line_id,
11885        });
11886        let (src_delta, scene_delta) = frontend
11887            .add_constraint(&ctx, version, sketch_id, constraint)
11888            .await
11889            .unwrap();
11890        assert_eq!(
11891            src_delta.text.as_str(),
11892            "\
11893sketch(on = XY) {
11894  point1 = point(at = [var 1, var 1])
11895  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
11896  midpoint(line1, point = point1)
11897}
11898"
11899        );
11900        assert_eq!(
11901            scene_delta.new_graph.objects.len(),
11902            7,
11903            "{:#?}",
11904            scene_delta.new_graph.objects
11905        );
11906
11907        ctx.close().await;
11908    }
11909
11910    #[tokio::test(flavor = "multi_thread")]
11911    async fn test_segments_symmetric() {
11912        let initial_source = "\
11913sketch(on = XY) {
11914  line(start = [var 0, var 0], end = [var 0, var 4])
11915  line(start = [var 4, var 0], end = [var 4, var 4])
11916  line(start = [var 2, var -1], end = [var 2, var 5])
11917}
11918";
11919
11920        let program = Program::parse(initial_source).unwrap().0.unwrap();
11921
11922        let mut frontend = FrontendState::new();
11923
11924        let ctx = ExecutorContext::new_mock(None).await;
11925        let version = Version(0);
11926
11927        frontend.program = program.clone();
11928        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11929        frontend.update_state_after_exec(outcome, true);
11930        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11931        let sketch_id = sketch_object.id;
11932        let sketch = expect_sketch(sketch_object);
11933        let line1_id = *sketch.segments.get(2).unwrap();
11934        let line2_id = *sketch.segments.get(5).unwrap();
11935        let axis_id = *sketch.segments.get(8).unwrap();
11936
11937        let constraint = Constraint::Symmetric(Symmetric {
11938            input: vec![line1_id, line2_id],
11939            axis: axis_id,
11940        });
11941        let (src_delta, scene_delta) = frontend
11942            .add_constraint(&ctx, version, sketch_id, constraint)
11943            .await
11944            .unwrap();
11945        assert_eq!(
11946            src_delta.text.as_str(),
11947            "\
11948sketch(on = XY) {
11949  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11950  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11951  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11952  symmetric([line1, line2], axis = line3)
11953}
11954"
11955        );
11956        assert_eq!(
11957            scene_delta.new_graph.objects.len(),
11958            12,
11959            "{:#?}",
11960            scene_delta.new_graph.objects
11961        );
11962
11963        ctx.close().await;
11964    }
11965
11966    #[tokio::test(flavor = "multi_thread")]
11967    async fn test_point_arc_midpoint() {
11968        let initial_source = "\
11969sketch(on = XY) {
11970  point(at = [var 6, var 3])
11971  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11972}
11973";
11974
11975        let program = Program::parse(initial_source).unwrap().0.unwrap();
11976
11977        let mut frontend = FrontendState::new();
11978
11979        let ctx = ExecutorContext::new_mock(None).await;
11980        let version = Version(0);
11981
11982        frontend.program = program.clone();
11983        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11984        frontend.update_state_after_exec(outcome, true);
11985        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11986        let sketch_id = sketch_object.id;
11987        let sketch = expect_sketch(sketch_object);
11988        let point_id = *sketch.segments.first().unwrap();
11989        let arc_id = *sketch.segments.get(4).unwrap();
11990
11991        let constraint = Constraint::Midpoint(Midpoint {
11992            point: point_id,
11993            segment: arc_id,
11994        });
11995        let (src_delta, scene_delta) = frontend
11996            .add_constraint(&ctx, version, sketch_id, constraint)
11997            .await
11998            .unwrap();
11999        assert_eq!(
12000            src_delta.text.as_str(),
12001            "\
12002sketch(on = XY) {
12003  point1 = point(at = [var 6, var 3])
12004  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12005  midpoint(arc1, point = point1)
12006}
12007"
12008        );
12009        assert_eq!(
12010            scene_delta.new_graph.objects.len(),
12011            8,
12012            "{:#?}",
12013            scene_delta.new_graph.objects
12014        );
12015
12016        ctx.close().await;
12017    }
12018
12019    #[tokio::test(flavor = "multi_thread")]
12020    async fn test_segments_symmetric_arcs() {
12021        let initial_source = "\
12022sketch(on = XY) {
12023  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12024  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12025  line(start = [var 0, var -10], end = [var 0, var 10])
12026}
12027";
12028
12029        let program = Program::parse(initial_source).unwrap().0.unwrap();
12030
12031        let mut frontend = FrontendState::new();
12032
12033        let ctx = ExecutorContext::new_mock(None).await;
12034        let version = Version(0);
12035
12036        frontend.program = program.clone();
12037        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12038        frontend.update_state_after_exec(outcome, true);
12039        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12040        let sketch_id = sketch_object.id;
12041        let sketch = expect_sketch(sketch_object);
12042        let arc1_id = *sketch.segments.get(3).unwrap();
12043        let arc2_id = *sketch.segments.get(7).unwrap();
12044        let axis_id = *sketch.segments.get(10).unwrap();
12045
12046        let constraint = Constraint::Symmetric(Symmetric {
12047            input: vec![arc1_id, arc2_id],
12048            axis: axis_id,
12049        });
12050        let (src_delta, scene_delta) = frontend
12051            .add_constraint(&ctx, version, sketch_id, constraint)
12052            .await
12053            .unwrap();
12054        assert_eq!(
12055            src_delta.text.as_str(),
12056            "\
12057sketch(on = XY) {
12058  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12059  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12060  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
12061  symmetric([arc1, arc2], axis = line1)
12062}
12063"
12064        );
12065        assert_eq!(
12066            scene_delta.new_graph.objects.len(),
12067            14,
12068            "{:#?}",
12069            scene_delta.new_graph.objects
12070        );
12071
12072        ctx.close().await;
12073    }
12074
12075    #[tokio::test(flavor = "multi_thread")]
12076    async fn test_sketch_on_face_simple() {
12077        let initial_source = "\
12078len = 2mm
12079cube = startSketchOn(XY)
12080  |> startProfile(at = [0, 0])
12081  |> line(end = [len, 0], tag = $side)
12082  |> line(end = [0, len])
12083  |> line(end = [-len, 0])
12084  |> line(end = [0, -len])
12085  |> close()
12086  |> extrude(length = len)
12087
12088face = faceOf(cube, face = side)
12089";
12090
12091        let program = Program::parse(initial_source).unwrap().0.unwrap();
12092
12093        let mut frontend = FrontendState::new();
12094
12095        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12096        let mock_ctx = ExecutorContext::new_mock(None).await;
12097        let version = Version(0);
12098
12099        frontend.hack_set_program(&ctx, program).await.unwrap();
12100        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
12101        let face_id = face_object.id;
12102
12103        let sketch_args = SketchCtor {
12104            on: Plane::Object(face_id),
12105        };
12106        let (_src_delta, scene_delta, sketch_id) = frontend
12107            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12108            .await
12109            .unwrap();
12110        assert_eq!(sketch_id, ObjectId(2));
12111        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12112        let sketch_object = &scene_delta.new_graph.objects[2];
12113        assert_eq!(sketch_object.id, ObjectId(2));
12114        assert_eq!(
12115            sketch_object.kind,
12116            ObjectKind::Sketch(Sketch {
12117                args: SketchCtor {
12118                    on: Plane::Object(face_id),
12119                },
12120                plane: face_id,
12121                segments: vec![],
12122                constraints: vec![],
12123            })
12124        );
12125        assert_eq!(scene_delta.new_graph.objects.len(), 8);
12126
12127        ctx.close().await;
12128        mock_ctx.close().await;
12129    }
12130
12131    #[tokio::test(flavor = "multi_thread")]
12132    async fn test_sketch_on_wall_artifact_from_region_extrude() {
12133        let initial_source = "\
12134s = sketch(on = YZ) {
12135  line1 = line(start = [0, 0], end = [0, 1])
12136  line2 = line(start = [0, 1], end = [1, 1])
12137  line3 = line(start = [1, 1], end = [0, 0])
12138}
12139region001 = region(point = [0.1, 0.1], sketch = s)
12140extrude001 = extrude(region001, length = 5)
12141";
12142
12143        let program = Program::parse(initial_source).unwrap().0.unwrap();
12144
12145        let mut frontend = FrontendState::new();
12146        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12147        let version = Version(0);
12148
12149        frontend.hack_set_program(&ctx, program).await.unwrap();
12150        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12151
12152        let sketch_args = SketchCtor {
12153            on: Plane::Object(wall_object_id),
12154        };
12155        let (src_delta, _scene_delta, _sketch_id) = frontend
12156            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12157            .await
12158            .unwrap();
12159        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12160
12161        ctx.close().await;
12162    }
12163
12164    #[tokio::test(flavor = "multi_thread")]
12165    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
12166        let initial_source = "\
12167sketch001 = sketch(on = YZ) {
12168  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
12169  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
12170  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
12171  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
12172  coincident([line1.end, line2.start])
12173  coincident([line2.end, line3.start])
12174  coincident([line3.end, line4.start])
12175  coincident([line4.end, line1.start])
12176  parallel([line2, line4])
12177  parallel([line3, line1])
12178  perpendicular([line1, line2])
12179  horizontal(line3)
12180  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
12181}
12182region001 = region(point = [3.1, 3.74], sketch = sketch001)
12183extrude001 = extrude(region001, length = 5)
12184";
12185
12186        let program = Program::parse(initial_source).unwrap().0.unwrap();
12187
12188        let mut frontend = FrontendState::new();
12189        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12190        let version = Version(0);
12191
12192        frontend.hack_set_program(&ctx, program).await.unwrap();
12193        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12194
12195        let sketch_args = SketchCtor {
12196            on: Plane::Object(wall_object_id),
12197        };
12198        let (src_delta, _scene_delta, _sketch_id) = frontend
12199            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12200            .await
12201            .unwrap();
12202        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12203
12204        ctx.close().await;
12205    }
12206
12207    #[tokio::test(flavor = "multi_thread")]
12208    async fn test_sketch_on_plane_incremental() {
12209        let initial_source = "\
12210len = 2mm
12211cube = startSketchOn(XY)
12212  |> startProfile(at = [0, 0])
12213  |> line(end = [len, 0], tag = $side)
12214  |> line(end = [0, len])
12215  |> line(end = [-len, 0])
12216  |> line(end = [0, -len])
12217  |> close()
12218  |> extrude(length = len)
12219
12220plane = planeOf(cube, face = side)
12221";
12222
12223        let program = Program::parse(initial_source).unwrap().0.unwrap();
12224
12225        let mut frontend = FrontendState::new();
12226
12227        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12228        let mock_ctx = ExecutorContext::new_mock(None).await;
12229        let version = Version(0);
12230
12231        frontend.hack_set_program(&ctx, program).await.unwrap();
12232        // Find the last plane since the first plane is the XY plane.
12233        let plane_object = frontend
12234            .scene_graph
12235            .objects
12236            .iter()
12237            .rev()
12238            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
12239            .unwrap();
12240        let plane_id = plane_object.id;
12241
12242        let sketch_args = SketchCtor {
12243            on: Plane::Object(plane_id),
12244        };
12245        let (src_delta, scene_delta, sketch_id) = frontend
12246            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12247            .await
12248            .unwrap();
12249        assert_eq!(
12250            src_delta.text.as_str(),
12251            "\
12252len = 2mm
12253cube = startSketchOn(XY)
12254  |> startProfile(at = [0, 0])
12255  |> line(end = [len, 0], tag = $side)
12256  |> line(end = [0, len])
12257  |> line(end = [-len, 0])
12258  |> line(end = [0, -len])
12259  |> close()
12260  |> extrude(length = len)
12261
12262plane = planeOf(cube, face = side)
12263sketch001 = sketch(on = plane) {
12264}
12265"
12266        );
12267        assert_eq!(sketch_id, ObjectId(2));
12268        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12269        let sketch_object = &scene_delta.new_graph.objects[2];
12270        assert_eq!(sketch_object.id, ObjectId(2));
12271        assert_eq!(
12272            sketch_object.kind,
12273            ObjectKind::Sketch(Sketch {
12274                args: SketchCtor {
12275                    on: Plane::Object(plane_id),
12276                },
12277                plane: plane_id,
12278                segments: vec![],
12279                constraints: vec![],
12280            })
12281        );
12282        assert_eq!(scene_delta.new_graph.objects.len(), 9);
12283
12284        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
12285        assert_eq!(plane_object.id, plane_id);
12286        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
12287
12288        ctx.close().await;
12289        mock_ctx.close().await;
12290    }
12291
12292    #[tokio::test(flavor = "multi_thread")]
12293    async fn test_new_sketch_uses_unique_variable_name() {
12294        let initial_source = "\
12295sketch1 = sketch(on = XY) {
12296}
12297";
12298
12299        let program = Program::parse(initial_source).unwrap().0.unwrap();
12300
12301        let mut frontend = FrontendState::new();
12302        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12303        let version = Version(0);
12304
12305        frontend.hack_set_program(&ctx, program).await.unwrap();
12306
12307        let sketch_args = SketchCtor {
12308            on: Plane::Default(PlaneName::Yz),
12309        };
12310        let (src_delta, _, _) = frontend
12311            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12312            .await
12313            .unwrap();
12314
12315        assert_eq!(
12316            src_delta.text.as_str(),
12317            "\
12318sketch1 = sketch(on = XY) {
12319}
12320sketch001 = sketch(on = YZ) {
12321}
12322"
12323        );
12324
12325        ctx.close().await;
12326    }
12327
12328    #[tokio::test(flavor = "multi_thread")]
12329    async fn test_new_sketch_twice_using_same_plane() {
12330        let initial_source = "\
12331sketch1 = sketch(on = XY) {
12332}
12333";
12334
12335        let program = Program::parse(initial_source).unwrap().0.unwrap();
12336
12337        let mut frontend = FrontendState::new();
12338        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12339        let version = Version(0);
12340
12341        frontend.hack_set_program(&ctx, program).await.unwrap();
12342
12343        let sketch_args = SketchCtor {
12344            on: Plane::Default(PlaneName::Xy),
12345        };
12346        let (src_delta, _, _) = frontend
12347            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12348            .await
12349            .unwrap();
12350
12351        assert_eq!(
12352            src_delta.text.as_str(),
12353            "\
12354sketch1 = sketch(on = XY) {
12355}
12356sketch001 = sketch(on = XY) {
12357}
12358"
12359        );
12360
12361        ctx.close().await;
12362    }
12363
12364    #[tokio::test(flavor = "multi_thread")]
12365    async fn test_sketch_mode_reuses_cached_on_expression() {
12366        let initial_source = "\
12367width = 2mm
12368sketch(on = offsetPlane(XY, offset = width)) {
12369  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
12370  distance([line1.start, line1.end]) == width
12371}
12372";
12373        let program = Program::parse(initial_source).unwrap().0.unwrap();
12374
12375        let mut frontend = FrontendState::new();
12376        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12377        let mock_ctx = ExecutorContext::new_mock(None).await;
12378        let version = Version(0);
12379        let project_id = ProjectId(0);
12380        let file_id = FileId(0);
12381
12382        frontend.hack_set_program(&ctx, program).await.unwrap();
12383        let initial_object_count = frontend.scene_graph.objects.len();
12384        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
12385            .expect("Expected sketch object to exist")
12386            .id;
12387
12388        // Entering sketch mode should reuse cached `on` expression state
12389        // (offsetPlane result), not fail or create extra on-surface objects.
12390        let scene_delta = frontend
12391            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12392            .await
12393            .unwrap();
12394        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12395
12396        // A follow-up sketch-mode execution should keep the same stable object
12397        // graph shape as well.
12398        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
12399        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12400
12401        ctx.close().await;
12402        mock_ctx.close().await;
12403    }
12404
12405    #[tokio::test(flavor = "multi_thread")]
12406    async fn test_execute_mock_from_preview_consumes_sketch_var_warm_starts() {
12407        let initial_source = "\
12408sketch(on = XY) {
12409  point(at = [var 1mm, var 2mm])
12410}
12411";
12412
12413        let program = Program::parse(initial_source).unwrap().0.unwrap();
12414
12415        let mut frontend = FrontendState::new();
12416        let mock_ctx = ExecutorContext::new_mock(None).await;
12417        let version = Version(0);
12418
12419        frontend.program = program.clone();
12420        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12421        frontend.update_state_after_exec(outcome, true);
12422        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12423        let sketch_id = sketch_object.id;
12424        frontend
12425            .sketch_var_warm_start_overrides
12426            .insert(sketch_id, vec![5.0, 6.0]);
12427
12428        let mut cold_frontend = frontend.clone();
12429        let (warm_src_delta, _) = frontend
12430            .execute_mock_from_preview(&mock_ctx, version, sketch_id)
12431            .await
12432            .unwrap();
12433        let (cold_src_delta, _) = cold_frontend
12434            .execute_mock_with_warm_starts(&mock_ctx, sketch_id, false)
12435            .await
12436            .unwrap();
12437
12438        assert_eq!(
12439            warm_src_delta.text.as_str(),
12440            "\
12441sketch(on = XY) {
12442  point(at = [var 5mm, var 6mm])
12443}
12444"
12445        );
12446        assert_eq!(
12447            cold_src_delta.text.as_str(),
12448            "\
12449sketch(on = XY) {
12450  point(at = [var 1mm, var 2mm])
12451}
12452"
12453        );
12454
12455        mock_ctx.close().await;
12456    }
12457
12458    #[tokio::test(flavor = "multi_thread")]
12459    async fn test_committed_edit_merges_edited_vars_into_existing_warm_starts() {
12460        let initial_source = "\
12461sketch(on = XY) {
12462  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12463  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12464  coincident([line1.end, line2.start])
12465  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12466  coincident([line2.end, line3.start])
12467  coincident([line3.end, line1.start])
12468  equalLength([line3, line1])
12469  equalLength([line1, line2])
12470  distance([line1.start, line1.end]) == 4mm
12471}
12472";
12473
12474        let program = Program::parse(initial_source).unwrap().0.unwrap();
12475
12476        let mut frontend = FrontendState::new();
12477        let mock_ctx = ExecutorContext::new_mock(None).await;
12478        let version = Version(0);
12479
12480        frontend.program = program.clone();
12481        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12482        let outcome = frontend.update_state_after_exec(outcome, true);
12483        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12484        let sketch_id = sketch_object.id;
12485        let sketch = expect_sketch(sketch_object);
12486        let point_id = *sketch.segments.first().unwrap();
12487        frontend.replace_sketch_var_warm_starts(sketch_id, &outcome);
12488        let previous_warm_starts = frontend
12489            .sketch_var_warm_start_overrides
12490            .get(&sketch_id)
12491            .expect("Expected initial warm starts")
12492            .clone();
12493
12494        let segments = vec![ExistingSegmentCtor {
12495            id: point_id,
12496            ctor: SegmentCtor::Point(PointCtor {
12497                position: Point2d {
12498                    x: Expr::Var(Number {
12499                        value: 1.0,
12500                        units: NumericSuffix::Mm,
12501                    }),
12502                    y: Expr::Var(Number {
12503                        value: 2.0,
12504                        units: NumericSuffix::Mm,
12505                    }),
12506                },
12507            }),
12508        }];
12509        frontend
12510            .edit_segments(&mock_ctx, version, sketch_id, segments)
12511            .await
12512            .unwrap();
12513        let warm_starts = frontend
12514            .sketch_var_warm_start_overrides
12515            .get(&sketch_id)
12516            .expect("Expected warm starts for edited sketch");
12517
12518        assert_eq!(&warm_starts[0..4], &[1.0, 2.0, 1.28, -0.78]);
12519        assert_eq!(&warm_starts[4..], &previous_warm_starts[4..]);
12520
12521        mock_ctx.close().await;
12522    }
12523
12524    #[tokio::test(flavor = "multi_thread")]
12525    async fn test_multiple_sketch_blocks() {
12526        let initial_source = "\
12527// Cube that requires the engine.
12528width = 2
12529sketch001 = startSketchOn(XY)
12530profile001 = startProfile(sketch001, at = [0, 0])
12531  |> yLine(length = width, tag = $seg1)
12532  |> xLine(length = width)
12533  |> yLine(length = -width)
12534  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12535  |> close()
12536extrude001 = extrude(profile001, length = width)
12537
12538// Get a value that requires the engine.
12539x = segLen(seg1)
12540
12541// Triangle with side length 2*x.
12542sketch(on = XY) {
12543  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12544  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12545  coincident([line1.end, line2.start])
12546  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12547  coincident([line2.end, line3.start])
12548  coincident([line3.end, line1.start])
12549  equalLength([line3, line1])
12550  equalLength([line1, line2])
12551  distance([line1.start, line1.end]) == 2*x
12552}
12553
12554// Line segment with length x.
12555sketch2 = sketch(on = XY) {
12556  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12557  distance([line1.start, line1.end]) == x
12558}
12559";
12560
12561        let program = Program::parse(initial_source).unwrap().0.unwrap();
12562
12563        let mut frontend = FrontendState::new();
12564
12565        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12566        let mock_ctx = ExecutorContext::new_mock(None).await;
12567        let version = Version(0);
12568        let project_id = ProjectId(0);
12569        let file_id = FileId(0);
12570
12571        frontend.hack_set_program(&ctx, program).await.unwrap();
12572        let sketch_objects = frontend
12573            .scene_graph
12574            .objects
12575            .iter()
12576            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12577            .collect::<Vec<_>>();
12578        let sketch1_id = sketch_objects.first().unwrap().id;
12579        let sketch2_id = sketch_objects.get(1).unwrap().id;
12580        // First point in sketch1.
12581        let point1_id = ObjectId(sketch1_id.0 + 1);
12582        // First point in sketch2.
12583        let point2_id = ObjectId(sketch2_id.0 + 1);
12584
12585        // Edit the first sketch. Objects before the sketch block should be
12586        // present from execution cache so that we can sketch on prior planes,
12587        // for example. Objects after the first sketch block should not be
12588        // present since those statements are skipped in sketch mode.
12589        //
12590        // - startSketchOn(XY) Plane 1
12591        // - sketch on=XY Plane 1
12592        // - Sketch block 16
12593        let scene_delta = frontend
12594            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12595            .await
12596            .unwrap();
12597        assert_eq!(
12598            scene_delta.new_graph.objects.len(),
12599            18,
12600            "{:#?}",
12601            scene_delta.new_graph.objects
12602        );
12603
12604        // Edit a point in the first sketch.
12605        let point_ctor = PointCtor {
12606            position: Point2d {
12607                x: Expr::Var(Number {
12608                    value: 1.0,
12609                    units: NumericSuffix::Mm,
12610                }),
12611                y: Expr::Var(Number {
12612                    value: 2.0,
12613                    units: NumericSuffix::Mm,
12614                }),
12615            },
12616        };
12617        let segments = vec![ExistingSegmentCtor {
12618            id: point1_id,
12619            ctor: SegmentCtor::Point(point_ctor),
12620        }];
12621        let (src_delta, _) = frontend
12622            .edit_segments(&mock_ctx, version, sketch1_id, segments)
12623            .await
12624            .unwrap();
12625        // Only the first sketch block changes.
12626        pretty_assertions::assert_eq!(
12627            "// Cube that requires the engine.\nwidth = 2\nsketch001 = startSketchOn(XY)\nprofile001 = startProfile(sketch001, at = [0, 0])\n  |> yLine(length = width, tag = $seg1)\n  |> xLine(length = width)\n  |> yLine(length = -width)\n  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n  |> close()\nextrude001 = extrude(profile001, length = width)\n\n// Get a value that requires the engine.\nx = segLen(seg1)\n\n// Triangle with side length 2*x.\nsketch(on = XY) {\n  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])\n  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])\n  coincident([line1.end, line2.start])\n  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])\n  coincident([line2.end, line3.start])\n  coincident([line3.end, line1.start])\n  equalLength([line3, line1])\n  equalLength([line1, line2])\n  distance([line1.start, line1.end]) == 2 * x\n}\n\n// Line segment with length x.\nsketch2 = sketch(on = XY) {\n  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])\n  distance([line1.start, line1.end]) == x\n}\n",
12628            src_delta.text.as_str(),
12629        );
12630
12631        // Execute mock to simulate drag end.
12632        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
12633        // Only the first sketch block changes.
12634        pretty_assertions::assert_eq!(
12635            src_delta.text.as_str(),
12636            "\
12637// Cube that requires the engine.
12638width = 2
12639sketch001 = startSketchOn(XY)
12640profile001 = startProfile(sketch001, at = [0, 0])
12641  |> yLine(length = width, tag = $seg1)
12642  |> xLine(length = width)
12643  |> yLine(length = -width)
12644  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12645  |> close()
12646extrude001 = extrude(profile001, length = width)
12647
12648// Get a value that requires the engine.
12649x = segLen(seg1)
12650
12651// Triangle with side length 2*x.
12652sketch(on = XY) {
12653  line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12654  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12655  coincident([line1.end, line2.start])
12656  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12657  coincident([line2.end, line3.start])
12658  coincident([line3.end, line1.start])
12659  equalLength([line3, line1])
12660  equalLength([line1, line2])
12661  distance([line1.start, line1.end]) == 2 * x
12662}
12663
12664// Line segment with length x.
12665sketch2 = sketch(on = XY) {
12666  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12667  distance([line1.start, line1.end]) == x
12668}
12669"
12670        );
12671        // Exit sketch. Objects from the entire program should be present.
12672        //
12673        // - startSketchOn(XY) Plane 1
12674        // - sketch on=XY Plane 1
12675        // - Sketch block 16
12676        // - sketch on=XY cached
12677        // - Sketch block 5
12678        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12679        pretty_assertions::assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
12680
12681        // Edit the second sketch.
12682        //
12683        // - startSketchOn(XY) Plane 1
12684        // - sketch on=XY Plane 1
12685        // - Sketch block 16
12686        // - sketch on=XY cached
12687        // - Sketch block 5
12688        let scene_delta = frontend
12689            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12690            .await
12691            .unwrap();
12692        pretty_assertions::assert_eq!(
12693            scene_delta.new_graph.objects.len(),
12694            24,
12695            "{:#?}",
12696            scene_delta.new_graph.objects
12697        );
12698
12699        // Edit a point in the second sketch.
12700        let point_ctor = PointCtor {
12701            position: Point2d {
12702                x: Expr::Var(Number {
12703                    value: 3.0,
12704                    units: NumericSuffix::Mm,
12705                }),
12706                y: Expr::Var(Number {
12707                    value: 4.0,
12708                    units: NumericSuffix::Mm,
12709                }),
12710            },
12711        };
12712        let segments = vec![ExistingSegmentCtor {
12713            id: point2_id,
12714            ctor: SegmentCtor::Point(point_ctor),
12715        }];
12716        let (src_delta, _) = frontend
12717            .edit_segments(&mock_ctx, version, sketch2_id, segments)
12718            .await
12719            .unwrap();
12720        // Only the second sketch block changes.
12721        pretty_assertions::assert_eq!(
12722            "\
12723// Cube that requires the engine.
12724width = 2
12725sketch001 = startSketchOn(XY)
12726profile001 = startProfile(sketch001, at = [0, 0])
12727  |> yLine(length = width, tag = $seg1)
12728  |> xLine(length = width)
12729  |> yLine(length = -width)
12730  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12731  |> close()
12732extrude001 = extrude(profile001, length = width)
12733
12734// Get a value that requires the engine.
12735x = segLen(seg1)
12736
12737// Triangle with side length 2*x.
12738sketch(on = XY) {
12739  line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12740  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12741  coincident([line1.end, line2.start])
12742  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12743  coincident([line2.end, line3.start])
12744  coincident([line3.end, line1.start])
12745  equalLength([line3, line1])
12746  equalLength([line1, line2])
12747  distance([line1.start, line1.end]) == 2 * x
12748}
12749
12750// Line segment with length x.
12751sketch2 = sketch(on = XY) {
12752  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12753  distance([line1.start, line1.end]) == x
12754}
12755",
12756            src_delta.text.as_str(),
12757        );
12758
12759        // Execute mock to simulate drag end.
12760        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
12761        // Only the second sketch block changes.
12762        pretty_assertions::assert_eq!(
12763            "\
12764// Cube that requires the engine.
12765width = 2
12766sketch001 = startSketchOn(XY)
12767profile001 = startProfile(sketch001, at = [0, 0])
12768  |> yLine(length = width, tag = $seg1)
12769  |> xLine(length = width)
12770  |> yLine(length = -width)
12771  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12772  |> close()
12773extrude001 = extrude(profile001, length = width)
12774
12775// Get a value that requires the engine.
12776x = segLen(seg1)
12777
12778// Triangle with side length 2*x.
12779sketch(on = XY) {
12780  line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12781  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12782  coincident([line1.end, line2.start])
12783  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12784  coincident([line2.end, line3.start])
12785  coincident([line3.end, line1.start])
12786  equalLength([line3, line1])
12787  equalLength([line1, line2])
12788  distance([line1.start, line1.end]) == 2 * x
12789}
12790
12791// Line segment with length x.
12792sketch2 = sketch(on = XY) {
12793  line1 = line(start = [var 2.12mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12794  distance([line1.start, line1.end]) == x
12795}
12796",
12797            src_delta.text.as_str(),
12798        );
12799
12800        ctx.close().await;
12801        mock_ctx.close().await;
12802    }
12803
12804    #[tokio::test(flavor = "multi_thread")]
12805    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
12806        clear_mem_cache().await;
12807
12808        let source = r#"sketch001 = sketch(on = XZ) {
12809  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
12810}
12811sketch002 = sketch(on = XY) {
12812  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
12813  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
12814  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
12815  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
12816  coincident([line1.end, line2.start])
12817  coincident([line2.end, line3.start])
12818  coincident([line3.end, line4.start])
12819  coincident([line4.end, line1.start])
12820  parallel([line2, line4])
12821  parallel([line3, line1])
12822  perpendicular([line1, line2])
12823  horizontal(line3)
12824  coincident([line1.start, ORIGIN])
12825}
12826"#;
12827
12828        let program = Program::parse(source).unwrap().0.unwrap();
12829        let mut frontend = FrontendState::new();
12830        let ctx = ExecutorContext::new_with_engine(
12831            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
12832            Default::default(),
12833        );
12834        let mock_ctx = ExecutorContext::new_mock(None).await;
12835        let version = Version(0);
12836        let project_id = ProjectId(0);
12837        let file_id = FileId(0);
12838
12839        frontend.hack_set_program(&ctx, program).await.unwrap();
12840        let sketch_objects = frontend
12841            .scene_graph
12842            .objects
12843            .iter()
12844            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
12845            .collect::<Vec<_>>();
12846        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
12847
12848        let sketch1_id = sketch_objects[0].id;
12849        let sketch2_id = sketch_objects[1].id;
12850
12851        frontend
12852            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12853            .await
12854            .unwrap();
12855        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12856
12857        let scene_delta = frontend
12858            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12859            .await
12860            .unwrap();
12861        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
12862
12863        clear_mem_cache().await;
12864        ctx.close().await;
12865        mock_ctx.close().await;
12866    }
12867
12868    // Regression tests: operations on source code with extra whitespace/newlines.
12869    // These test that NodePath-based lookups work correctly when source ranges
12870    // are shifted by extra whitespace that wouldn't be present after formatting.
12871
12872    #[tokio::test(flavor = "multi_thread")]
12873    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
12874        // Extra newlines after @settings line - this shifts all source ranges.
12875        let initial_source = "@settings(defaultLengthUnit = mm)
12876
12877
12878
12879sketch001 = sketch(on = XY) {
12880  point(at = [1in, 2in])
12881}
12882";
12883
12884        let program = Program::parse(initial_source).unwrap().0.unwrap();
12885        let mut frontend = FrontendState::new();
12886
12887        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12888        let mock_ctx = ExecutorContext::new_mock(None).await;
12889        let version = Version(0);
12890        let project_id = ProjectId(0);
12891        let file_id = FileId(0);
12892
12893        frontend.hack_set_program(&ctx, program).await.unwrap();
12894        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12895        let sketch_id = sketch_object.id;
12896
12897        // Edit sketch should succeed despite extra newlines.
12898        frontend
12899            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12900            .await
12901            .unwrap();
12902
12903        // Add a new point to the sketch.
12904        let point_ctor = PointCtor {
12905            position: Point2d {
12906                x: Expr::Number(Number {
12907                    value: 5.0,
12908                    units: NumericSuffix::Mm,
12909                }),
12910                y: Expr::Number(Number {
12911                    value: 6.0,
12912                    units: NumericSuffix::Mm,
12913                }),
12914            },
12915        };
12916        let segment = SegmentCtor::Point(point_ctor);
12917        let (src_delta, scene_delta) = frontend
12918            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12919            .await
12920            .unwrap();
12921        // After adding a point, the source should be reformatted with standard whitespace.
12922        assert!(
12923            src_delta.text.contains("point(at = [5mm, 6mm])"),
12924            "Expected new point in source, got: {}",
12925            src_delta.text
12926        );
12927        assert!(!scene_delta.new_objects.is_empty());
12928
12929        ctx.close().await;
12930        mock_ctx.close().await;
12931    }
12932
12933    #[tokio::test(flavor = "multi_thread")]
12934    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
12935        // Extra newlines after @settings, with an empty sketch block.
12936        let initial_source = "@settings(defaultLengthUnit = mm)
12937
12938
12939
12940s = sketch(on = XY) {}
12941";
12942
12943        let program = Program::parse(initial_source).unwrap().0.unwrap();
12944        let mut frontend = FrontendState::new();
12945
12946        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12947        let mock_ctx = ExecutorContext::new_mock(None).await;
12948        let version = Version(0);
12949
12950        frontend.hack_set_program(&ctx, program).await.unwrap();
12951        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12952        let sketch_id = sketch_object.id;
12953
12954        let line_ctor = LineCtor {
12955            start: Point2d {
12956                x: Expr::Number(Number {
12957                    value: 0.0,
12958                    units: NumericSuffix::Mm,
12959                }),
12960                y: Expr::Number(Number {
12961                    value: 0.0,
12962                    units: NumericSuffix::Mm,
12963                }),
12964            },
12965            end: Point2d {
12966                x: Expr::Number(Number {
12967                    value: 10.0,
12968                    units: NumericSuffix::Mm,
12969                }),
12970                y: Expr::Number(Number {
12971                    value: 10.0,
12972                    units: NumericSuffix::Mm,
12973                }),
12974            },
12975            construction: None,
12976        };
12977        let segment = SegmentCtor::Line(line_ctor);
12978        let (src_delta, scene_delta) = frontend
12979            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12980            .await
12981            .unwrap();
12982        assert!(
12983            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12984            "Expected line in source, got: {}",
12985            src_delta.text
12986        );
12987        // Line creates start point, end point, and line segment.
12988        assert_eq!(scene_delta.new_objects.len(), 3);
12989
12990        ctx.close().await;
12991        mock_ctx.close().await;
12992    }
12993
12994    #[tokio::test(flavor = "multi_thread")]
12995    async fn test_extra_newlines_between_operations_edit_line() {
12996        // Extra newlines between @settings and sketch, and inside the sketch block.
12997        let initial_source = "@settings(defaultLengthUnit = mm)
12998
12999
13000sketch001 = sketch(on = XY) {
13001
13002  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13003
13004}
13005";
13006
13007        let program = Program::parse(initial_source).unwrap().0.unwrap();
13008        let mut frontend = FrontendState::new();
13009
13010        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13011        let mock_ctx = ExecutorContext::new_mock(None).await;
13012        let version = Version(0);
13013        let project_id = ProjectId(0);
13014        let file_id = FileId(0);
13015
13016        frontend.hack_set_program(&ctx, program).await.unwrap();
13017        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13018        let sketch_id = sketch_object.id;
13019        let sketch = expect_sketch(sketch_object);
13020
13021        // Extract segment IDs before edit_sketch borrows frontend mutably.
13022        let line_id = sketch
13023            .segments
13024            .iter()
13025            .copied()
13026            .find(|seg_id| {
13027                matches!(
13028                    &frontend.scene_graph.objects[seg_id.0].kind,
13029                    ObjectKind::Segment {
13030                        segment: Segment::Line(_)
13031                    }
13032                )
13033            })
13034            .expect("Expected a line segment in sketch");
13035
13036        // Enter sketch edit mode.
13037        frontend
13038            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13039            .await
13040            .unwrap();
13041
13042        // Edit the line.
13043        let line_ctor = LineCtor {
13044            start: Point2d {
13045                x: Expr::Var(Number {
13046                    value: 1.0,
13047                    units: NumericSuffix::Mm,
13048                }),
13049                y: Expr::Var(Number {
13050                    value: 2.0,
13051                    units: NumericSuffix::Mm,
13052                }),
13053            },
13054            end: Point2d {
13055                x: Expr::Var(Number {
13056                    value: 13.0,
13057                    units: NumericSuffix::Mm,
13058                }),
13059                y: Expr::Var(Number {
13060                    value: 14.0,
13061                    units: NumericSuffix::Mm,
13062                }),
13063            },
13064            construction: None,
13065        };
13066        let segments = vec![ExistingSegmentCtor {
13067            id: line_id,
13068            ctor: SegmentCtor::Line(line_ctor),
13069        }];
13070        let (src_delta, _scene_delta) = frontend
13071            .edit_segments(&mock_ctx, version, sketch_id, segments)
13072            .await
13073            .unwrap();
13074        assert!(
13075            src_delta
13076                .text
13077                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
13078            "Expected edited line in source, got: {}",
13079            src_delta.text
13080        );
13081
13082        ctx.close().await;
13083        mock_ctx.close().await;
13084    }
13085
13086    #[tokio::test(flavor = "multi_thread")]
13087    async fn test_extra_newlines_delete_segment() {
13088        // Extra whitespace before and after the sketch block.
13089        let initial_source = "@settings(defaultLengthUnit = mm)
13090
13091
13092
13093sketch001 = sketch(on = XY) {
13094  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13095}
13096";
13097
13098        let program = Program::parse(initial_source).unwrap().0.unwrap();
13099        let mut frontend = FrontendState::new();
13100
13101        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13102        let mock_ctx = ExecutorContext::new_mock(None).await;
13103        let version = Version(0);
13104
13105        frontend.hack_set_program(&ctx, program).await.unwrap();
13106        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13107        let sketch_id = sketch_object.id;
13108        let sketch = expect_sketch(sketch_object);
13109
13110        // The sketch should have 3 segments: start point, center point, and the circle.
13111        assert_eq!(sketch.segments.len(), 3);
13112        let circle_id = sketch.segments[2];
13113
13114        // Delete the circle despite extra newlines in original source.
13115        let (src_delta, scene_delta) = frontend
13116            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
13117            .await
13118            .unwrap();
13119        assert!(
13120            src_delta.text.contains("sketch(on = XY) {"),
13121            "Expected sketch block in source, got: {}",
13122            src_delta.text
13123        );
13124        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13125        let new_sketch = expect_sketch(new_sketch_object);
13126        assert_eq!(new_sketch.segments.len(), 0);
13127
13128        ctx.close().await;
13129        mock_ctx.close().await;
13130    }
13131
13132    #[tokio::test(flavor = "multi_thread")]
13133    async fn test_unformatted_source_add_arc() {
13134        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
13135        let initial_source = "@settings(defaultLengthUnit = mm)
13136
13137
13138
13139
13140sketch001 = sketch(on = XY) {
13141}
13142";
13143
13144        let program = Program::parse(initial_source).unwrap().0.unwrap();
13145        let mut frontend = FrontendState::new();
13146
13147        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13148        let mock_ctx = ExecutorContext::new_mock(None).await;
13149        let version = Version(0);
13150
13151        frontend.hack_set_program(&ctx, program).await.unwrap();
13152        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13153        let sketch_id = sketch_object.id;
13154
13155        let arc_ctor = ArcCtor {
13156            start: Point2d {
13157                x: Expr::Var(Number {
13158                    value: 5.0,
13159                    units: NumericSuffix::Mm,
13160                }),
13161                y: Expr::Var(Number {
13162                    value: 0.0,
13163                    units: NumericSuffix::Mm,
13164                }),
13165            },
13166            end: Point2d {
13167                x: Expr::Var(Number {
13168                    value: 0.0,
13169                    units: NumericSuffix::Mm,
13170                }),
13171                y: Expr::Var(Number {
13172                    value: 5.0,
13173                    units: NumericSuffix::Mm,
13174                }),
13175            },
13176            center: Point2d {
13177                x: Expr::Var(Number {
13178                    value: 0.0,
13179                    units: NumericSuffix::Mm,
13180                }),
13181                y: Expr::Var(Number {
13182                    value: 0.0,
13183                    units: NumericSuffix::Mm,
13184                }),
13185            },
13186            construction: None,
13187        };
13188        let segment = SegmentCtor::Arc(arc_ctor);
13189        let (src_delta, scene_delta) = frontend
13190            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13191            .await
13192            .unwrap();
13193        assert!(
13194            src_delta
13195                .text
13196                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
13197            "Expected arc in source, got: {}",
13198            src_delta.text
13199        );
13200        assert!(!scene_delta.new_objects.is_empty());
13201
13202        ctx.close().await;
13203        mock_ctx.close().await;
13204    }
13205
13206    #[tokio::test(flavor = "multi_thread")]
13207    async fn test_extra_newlines_add_circle() {
13208        // Extra blank lines between settings and sketch.
13209        let initial_source = "@settings(defaultLengthUnit = mm)
13210
13211
13212
13213sketch001 = sketch(on = XY) {
13214}
13215";
13216
13217        let program = Program::parse(initial_source).unwrap().0.unwrap();
13218        let mut frontend = FrontendState::new();
13219
13220        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13221        let mock_ctx = ExecutorContext::new_mock(None).await;
13222        let version = Version(0);
13223
13224        frontend.hack_set_program(&ctx, program).await.unwrap();
13225        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13226        let sketch_id = sketch_object.id;
13227
13228        let circle_ctor = CircleCtor {
13229            start: Point2d {
13230                x: Expr::Var(Number {
13231                    value: 5.0,
13232                    units: NumericSuffix::Mm,
13233                }),
13234                y: Expr::Var(Number {
13235                    value: 0.0,
13236                    units: NumericSuffix::Mm,
13237                }),
13238            },
13239            center: Point2d {
13240                x: Expr::Var(Number {
13241                    value: 0.0,
13242                    units: NumericSuffix::Mm,
13243                }),
13244                y: Expr::Var(Number {
13245                    value: 0.0,
13246                    units: NumericSuffix::Mm,
13247                }),
13248            },
13249            construction: None,
13250        };
13251        let segment = SegmentCtor::Circle(circle_ctor);
13252        let (src_delta, scene_delta) = frontend
13253            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13254            .await
13255            .unwrap();
13256        assert!(
13257            src_delta
13258                .text
13259                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
13260            "Expected circle in source, got: {}",
13261            src_delta.text
13262        );
13263        assert!(!scene_delta.new_objects.is_empty());
13264
13265        ctx.close().await;
13266        mock_ctx.close().await;
13267    }
13268
13269    #[tokio::test(flavor = "multi_thread")]
13270    async fn test_extra_newlines_add_constraint() {
13271        // Extra newlines with a sketch containing two lines - add a coincident constraint.
13272        let initial_source = "@settings(defaultLengthUnit = mm)
13273
13274
13275
13276sketch001 = sketch(on = XY) {
13277  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13278  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
13279}
13280";
13281
13282        let program = Program::parse(initial_source).unwrap().0.unwrap();
13283        let mut frontend = FrontendState::new();
13284
13285        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13286        let mock_ctx = ExecutorContext::new_mock(None).await;
13287        let version = Version(0);
13288        let project_id = ProjectId(0);
13289        let file_id = FileId(0);
13290
13291        frontend.hack_set_program(&ctx, program).await.unwrap();
13292        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13293        let sketch_id = sketch_object.id;
13294        let sketch = expect_sketch(sketch_object);
13295
13296        // Extract segment data before edit_sketch borrows frontend mutably.
13297        let line_ids: Vec<ObjectId> = sketch
13298            .segments
13299            .iter()
13300            .copied()
13301            .filter(|seg_id| {
13302                matches!(
13303                    &frontend.scene_graph.objects[seg_id.0].kind,
13304                    ObjectKind::Segment {
13305                        segment: Segment::Line(_)
13306                    }
13307                )
13308            })
13309            .collect();
13310        assert_eq!(line_ids.len(), 2, "Expected two line segments");
13311
13312        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
13313        let ObjectKind::Segment {
13314            segment: Segment::Line(line1_data),
13315        } = &line1.kind
13316        else {
13317            panic!("Expected line");
13318        };
13319        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
13320        let ObjectKind::Segment {
13321            segment: Segment::Line(line2_data),
13322        } = &line2.kind
13323        else {
13324            panic!("Expected line");
13325        };
13326
13327        // Build constraint before entering sketch mode.
13328        let constraint = Constraint::Coincident(Coincident {
13329            segments: vec![line1_data.end.into(), line2_data.start.into()],
13330        });
13331
13332        // Enter sketch edit mode.
13333        frontend
13334            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13335            .await
13336            .unwrap();
13337        let (src_delta, _scene_delta) = frontend
13338            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13339            .await
13340            .unwrap();
13341        assert!(
13342            src_delta.text.contains("coincident("),
13343            "Expected coincident constraint in source, got: {}",
13344            src_delta.text
13345        );
13346
13347        ctx.close().await;
13348        mock_ctx.close().await;
13349    }
13350
13351    #[tokio::test(flavor = "multi_thread")]
13352    async fn test_extra_newlines_add_line_then_edit_line() {
13353        // Extra newlines after @settings - add a line, then edit it.
13354        let initial_source = "@settings(defaultLengthUnit = mm)
13355
13356
13357
13358sketch001 = sketch(on = XY) {
13359}
13360";
13361
13362        let program = Program::parse(initial_source).unwrap().0.unwrap();
13363        let mut frontend = FrontendState::new();
13364
13365        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13366        let mock_ctx = ExecutorContext::new_mock(None).await;
13367        let version = Version(0);
13368
13369        frontend.hack_set_program(&ctx, program).await.unwrap();
13370        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13371        let sketch_id = sketch_object.id;
13372
13373        // Add a line.
13374        let line_ctor = LineCtor {
13375            start: Point2d {
13376                x: Expr::Number(Number {
13377                    value: 0.0,
13378                    units: NumericSuffix::Mm,
13379                }),
13380                y: Expr::Number(Number {
13381                    value: 0.0,
13382                    units: NumericSuffix::Mm,
13383                }),
13384            },
13385            end: Point2d {
13386                x: Expr::Number(Number {
13387                    value: 10.0,
13388                    units: NumericSuffix::Mm,
13389                }),
13390                y: Expr::Number(Number {
13391                    value: 10.0,
13392                    units: NumericSuffix::Mm,
13393                }),
13394            },
13395            construction: None,
13396        };
13397        let segment = SegmentCtor::Line(line_ctor);
13398        let (src_delta, scene_delta) = frontend
13399            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13400            .await
13401            .unwrap();
13402        assert!(
13403            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13404            "Expected line in source after add, got: {}",
13405            src_delta.text
13406        );
13407        // Line creates start point, end point, and line segment.
13408        let line_id = *scene_delta.new_objects.last().unwrap();
13409
13410        // Edit the line.
13411        let line_ctor = LineCtor {
13412            start: Point2d {
13413                x: Expr::Number(Number {
13414                    value: 1.0,
13415                    units: NumericSuffix::Mm,
13416                }),
13417                y: Expr::Number(Number {
13418                    value: 2.0,
13419                    units: NumericSuffix::Mm,
13420                }),
13421            },
13422            end: Point2d {
13423                x: Expr::Number(Number {
13424                    value: 13.0,
13425                    units: NumericSuffix::Mm,
13426                }),
13427                y: Expr::Number(Number {
13428                    value: 14.0,
13429                    units: NumericSuffix::Mm,
13430                }),
13431            },
13432            construction: None,
13433        };
13434        let segments = vec![ExistingSegmentCtor {
13435            id: line_id,
13436            ctor: SegmentCtor::Line(line_ctor),
13437        }];
13438        let (src_delta, scene_delta) = frontend
13439            .edit_segments(&mock_ctx, version, sketch_id, segments)
13440            .await
13441            .unwrap();
13442        assert!(
13443            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
13444            "Expected edited line in source, got: {}",
13445            src_delta.text
13446        );
13447        assert_eq!(scene_delta.new_objects, vec![]);
13448
13449        ctx.close().await;
13450        mock_ctx.close().await;
13451    }
13452}