Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19use crate::execution::Artifact;
20use crate::execution::ArtifactGraph;
21use crate::execution::CapSubType;
22use crate::execution::MockConfig;
23use crate::execution::SKETCH_BLOCK_PARAM_ON;
24use crate::execution::annotations::WarningLevel;
25use crate::execution::cache::SketchModeState;
26use crate::execution::cache::clear_mem_cache;
27use crate::execution::cache::read_old_memory;
28use crate::execution::cache::write_old_memory;
29use crate::fmt::format_number_literal;
30use crate::front::Angle;
31use crate::front::ArcCtor;
32use crate::front::CircleCtor;
33use crate::front::ControlPointSplineCtor;
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;
87use crate::walk::NodeMut;
88use crate::walk::Visitable;
89
90pub(crate) mod api;
91pub(crate) mod modify;
92pub(crate) mod sketch;
93
94pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
95
96#[derive(Debug, Clone)]
97struct SketchCheckpoint {
98    id: SketchCheckpointId,
99    source: SourceDelta,
100    program: Program,
101    scene_graph: SceneGraph,
102    exec_outcome: ExecOutcome,
103    point_freedom_cache: HashMap<ObjectId, Freedom>,
104    mock_memory: Option<SketchModeState>,
105}
106mod traverse;
107pub(crate) mod trim;
108
109struct ArcSizeConstraintParams {
110    points: Vec<ObjectId>,
111    function_name: &'static str,
112    value: f64,
113    units: NumericSuffix,
114    label_position: Option<Point2d<Number>>,
115    constraint_type_name: &'static str,
116}
117
118const POINT_FN: &str = "point";
119const POINT_AT_PARAM: &str = "at";
120const LINE_FN: &str = "line";
121const LINE_VARIABLE: &str = "line";
122const LINE_START_PARAM: &str = "start";
123const LINE_END_PARAM: &str = "end";
124const ARC_FN: &str = "arc";
125const ARC_VARIABLE: &str = "arc";
126const ARC_START_PARAM: &str = "start";
127const ARC_END_PARAM: &str = "end";
128const ARC_CENTER_PARAM: &str = "center";
129const CIRCLE_FN: &str = "circle";
130const CIRCLE_VARIABLE: &str = "circle";
131const CIRCLE_START_PARAM: &str = "start";
132const CIRCLE_CENTER_PARAM: &str = "center";
133const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
134const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
135const LABEL_POSITION_PARAM: &str = "labelPosition";
136
137const COINCIDENT_FN: &str = "coincident";
138const DIAMETER_FN: &str = "diameter";
139const DISTANCE_FN: &str = "distance";
140const FIXED_FN: &str = "fixed";
141const ANGLE_FN: &str = "angle";
142const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
143const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
144const EQUAL_LENGTH_FN: &str = "equalLength";
145const EQUAL_RADIUS_FN: &str = "equalRadius";
146const HORIZONTAL_FN: &str = "horizontal";
147const MIDPOINT_FN: &str = "midpoint";
148const MIDPOINT_POINT_PARAM: &str = "point";
149const RADIUS_FN: &str = "radius";
150const SYMMETRIC_FN: &str = "symmetric";
151const SYMMETRIC_AXIS_PARAM: &str = "axis";
152const TANGENT_FN: &str = "tangent";
153const VERTICAL_FN: &str = "vertical";
154
155const LINE_PROPERTY_START: &str = "start";
156const LINE_PROPERTY_END: &str = "end";
157
158const ARC_PROPERTY_START: &str = "start";
159const ARC_PROPERTY_END: &str = "end";
160const ARC_PROPERTY_CENTER: &str = "center";
161const CIRCLE_PROPERTY_START: &str = "start";
162const CIRCLE_PROPERTY_CENTER: &str = "center";
163const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
164const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
165
166const CONSTRUCTION_PARAM: &str = "construction";
167
168#[derive(Debug, Clone, Copy)]
169enum EditDeleteKind {
170    Edit,
171    DeleteNonSketch,
172}
173
174impl EditDeleteKind {
175    /// Returns true if this edit is any type of deletion.
176    fn is_delete(&self) -> bool {
177        match self {
178            EditDeleteKind::Edit => false,
179            EditDeleteKind::DeleteNonSketch => true,
180        }
181    }
182
183    fn to_change_kind(self) -> ChangeKind {
184        match self {
185            EditDeleteKind::Edit => ChangeKind::Edit,
186            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
187        }
188    }
189}
190
191#[derive(Debug, Clone, Copy)]
192enum ChangeKind {
193    Add,
194    Edit,
195    Delete,
196    None,
197}
198
199#[derive(Debug, Clone, Serialize, ts_rs::TS)]
200#[ts(export, export_to = "FrontendApi.ts")]
201#[serde(tag = "type")]
202pub enum SetProgramOutcome {
203    #[serde(rename_all = "camelCase")]
204    Success {
205        scene_graph: Box<SceneGraph>,
206        exec_outcome: Box<ExecOutcome>,
207        checkpoint_id: Option<SketchCheckpointId>,
208    },
209    #[serde(rename_all = "camelCase")]
210    ExecFailure { error: Box<KclErrorWithOutputs> },
211}
212
213#[derive(Debug, Clone)]
214pub struct FrontendState {
215    program: Program,
216    scene_graph: SceneGraph,
217    /// Stores the last known freedom value for each point object.
218    /// This allows us to preserve freedom values when freedom analysis isn't run.
219    point_freedom_cache: HashMap<ObjectId, Freedom>,
220    sketch_checkpoints: VecDeque<SketchCheckpoint>,
221    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
222}
223
224impl Default for FrontendState {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230impl FrontendState {
231    pub fn new() -> Self {
232        Self {
233            program: Program::empty(),
234            scene_graph: SceneGraph {
235                project: ProjectId(0),
236                file: FileId(0),
237                version: Version(0),
238                objects: Default::default(),
239                settings: Default::default(),
240                sketch_mode: Default::default(),
241            },
242            point_freedom_cache: HashMap::new(),
243            sketch_checkpoints: VecDeque::new(),
244            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
245        }
246    }
247
248    /// Get a reference to the scene graph
249    pub fn scene_graph(&self) -> &SceneGraph {
250        &self.scene_graph
251    }
252
253    pub fn default_length_unit(&self) -> UnitLength {
254        self.program
255            .meta_settings()
256            .ok()
257            .flatten()
258            .map(|settings| settings.default_length_units)
259            .unwrap_or(UnitLength::Millimeters)
260    }
261
262    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
263        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
264
265        let checkpoint = SketchCheckpoint {
266            id: checkpoint_id,
267            source: SourceDelta {
268                text: source_from_ast(&self.program.ast),
269            },
270            program: self.program.clone(),
271            scene_graph: self.scene_graph.clone(),
272            exec_outcome,
273            point_freedom_cache: self.point_freedom_cache.clone(),
274            mock_memory: read_old_memory().await,
275        };
276
277        self.sketch_checkpoints.push_back(checkpoint);
278        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
279            self.sketch_checkpoints.pop_front();
280        }
281
282        Ok(checkpoint_id)
283    }
284
285    pub async fn restore_sketch_checkpoint(
286        &mut self,
287        checkpoint_id: SketchCheckpointId,
288    ) -> api::Result<RestoreSketchCheckpointOutcome> {
289        let checkpoint = self
290            .sketch_checkpoints
291            .iter()
292            .find(|checkpoint| checkpoint.id == checkpoint_id)
293            .cloned()
294            .ok_or_else(|| Error {
295                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
296            })?;
297
298        self.program = checkpoint.program;
299        self.scene_graph = checkpoint.scene_graph.clone();
300        self.point_freedom_cache = checkpoint.point_freedom_cache;
301
302        if let Some(mock_memory) = checkpoint.mock_memory {
303            write_old_memory(mock_memory).await;
304        } else {
305            clear_mem_cache().await;
306        }
307
308        Ok(RestoreSketchCheckpointOutcome {
309            source_delta: checkpoint.source,
310            scene_graph_delta: SceneGraphDelta {
311                new_graph: self.scene_graph_for_ui(),
312                new_objects: Vec::new(),
313                invalidates_ids: true,
314                exec_outcome: checkpoint.exec_outcome,
315            },
316        })
317    }
318
319    pub fn clear_sketch_checkpoints(&mut self) {
320        self.sketch_checkpoints.clear();
321    }
322    fn scene_graph_for_ui(&self) -> SceneGraph {
323        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
324            matches!(
325                object.kind,
326                ObjectKind::Segment {
327                    segment: Segment::ControlPointSpline(_)
328                }
329            )
330        });
331
332        if !has_control_point_splines {
333            return self.scene_graph.clone();
334        }
335
336        let hidden_constraint_ids = self
337            .scene_graph
338            .objects
339            .iter()
340            .filter_map(|object| match &object.kind {
341                ObjectKind::Constraint {
342                    constraint: Constraint::Coincident(coincident),
343                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
344                    Some(object.id)
345                }
346                _ => None,
347            })
348            .collect::<HashSet<_>>();
349
350        if hidden_constraint_ids.is_empty() {
351            return self.scene_graph.clone();
352        }
353
354        let mut scene_graph = self.scene_graph.clone();
355        for object in &mut scene_graph.objects {
356            match &mut object.kind {
357                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
358                    object.kind = ObjectKind::Nil;
359                }
360                ObjectKind::Sketch(sketch) => {
361                    sketch
362                        .constraints
363                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
364                }
365                _ => {}
366            }
367        }
368
369        scene_graph
370    }
371}
372
373fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
374    let mut first_owner_id = None;
375    for segment_id in coincident.segment_ids() {
376        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
377            return false;
378        };
379
380        match first_owner_id {
381            Some(first_owner_id) if first_owner_id != owner_id => return false,
382            Some(_) => {}
383            None => first_owner_id = Some(owner_id),
384        }
385    }
386
387    first_owner_id.is_some()
388}
389
390fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
391    let object = scene_graph.objects.get(segment_id.0)?;
392    let ObjectKind::Segment { segment } = &object.kind else {
393        return None;
394    };
395
396    match segment {
397        Segment::ControlPointSpline(_) => Some(segment_id),
398        Segment::Point(point) => point
399            .owner
400            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
401        Segment::Line(line) => line
402            .owner
403            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
404        _ => None,
405    }
406}
407
408fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
409    matches!(
410        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
411        Some(ObjectKind::Segment {
412            segment: Segment::ControlPointSpline(_)
413        })
414    )
415}
416
417fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
418    let experimental_features_allowed = program
419        .meta_settings()
420        .ok()
421        .flatten()
422        .map(|settings| settings.experimental_features == WarningLevel::Allow)
423        .unwrap_or(false);
424    if experimental_features_allowed {
425        return Ok(program.clone());
426    }
427
428    program.change_experimental_features(Some(WarningLevel::Allow))
429}
430
431impl SketchApi for FrontendState {
432    async fn execute_mock(
433        &mut self,
434        ctx: &ExecutorContext,
435        _version: Version,
436        sketch: ObjectId,
437    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
438        let sketch_block_ref =
439            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
440
441        let mut truncated_program = self.program.clone();
442        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
443            .map_err(KclErrorWithOutputs::no_outputs)?;
444
445        // Execute.
446        let outcome = ctx
447            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
448            .await?;
449        let new_source = source_from_ast(&self.program.ast);
450        let src_delta = SourceDelta { text: new_source };
451        // MockConfig::default() has freedom_analysis: true
452        let outcome = self.update_state_after_exec(outcome, true);
453        let scene_graph_delta = SceneGraphDelta {
454            new_graph: self.scene_graph.clone(),
455            new_objects: Default::default(),
456            invalidates_ids: false,
457            exec_outcome: outcome,
458        };
459        Ok((src_delta, scene_graph_delta))
460    }
461
462    async fn new_sketch(
463        &mut self,
464        ctx: &ExecutorContext,
465        _project: ProjectId,
466        _file: FileId,
467        _version: Version,
468        args: SketchCtor,
469    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
470        // TODO: Check version.
471
472        let mut new_ast = self.program.ast.clone();
473        // Create updated KCL source from args.
474        let mut plane_ast =
475            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
476        let mut defined_names = find_defined_names(&new_ast);
477        let is_face_of_expr = matches!(
478            &plane_ast,
479            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
480        );
481        if is_face_of_expr {
482            let face_name = next_free_name_with_padding("face", &defined_names)
483                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
484            let face_decl = ast::VariableDeclaration::new(
485                ast::VariableDeclarator::new(&face_name, plane_ast),
486                ast::ItemVisibility::Default,
487                ast::VariableKind::Const,
488            );
489            new_ast
490                .body
491                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
492                    face_decl,
493                ))));
494            defined_names.insert(face_name.clone());
495            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
496        }
497        let sketch_ast = ast::SketchBlock {
498            arguments: vec![ast::LabeledArg {
499                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
500                arg: plane_ast,
501            }],
502            body: Default::default(),
503            is_being_edited: false,
504            non_code_meta: Default::default(),
505            digest: None,
506        };
507        // Add a sketch block as a variable declaration directly, avoiding
508        // source-range mutation on a no-src node.
509        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
510            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
511        let sketch_decl = ast::VariableDeclaration::new(
512            ast::VariableDeclarator::new(
513                &sketch_name,
514                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
515            ),
516            ast::ItemVisibility::Default,
517            ast::VariableKind::Const,
518        );
519        new_ast
520            .body
521            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
522                sketch_decl,
523            ))));
524        // Convert to string source to create real source ranges.
525        let new_source = source_from_ast(&new_ast);
526        // Parse the new source.
527        let (new_program, errors) = Program::parse(&new_source)
528            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
529        if !errors.is_empty() {
530            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
531                "Error parsing KCL source after adding sketch: {errors:?}"
532            ))));
533        }
534        let Some(new_program) = new_program else {
535            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
536                "No AST produced after adding sketch".to_owned(),
537            )));
538        };
539
540        // Make sure to only set this if there are no errors.
541        self.program = new_program.clone();
542
543        // We need to do an engine execute so that the plane object gets created
544        // and is cached.
545        let outcome = ctx.run_with_caching(new_program.clone()).await?;
546        let freedom_analysis_ran = true;
547
548        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
549
550        let Some(sketch_id) = self
551            .scene_graph
552            .objects
553            .iter()
554            .filter_map(|object| match object.kind {
555                ObjectKind::Sketch(_) => Some(object.id),
556                _ => None,
557            })
558            .max_by_key(|id| id.0)
559        else {
560            return Err(KclErrorWithOutputs::from_error_outcome(
561                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
562                outcome,
563            ));
564        };
565        // Store the object in the scene.
566        self.scene_graph.sketch_mode = Some(sketch_id);
567
568        let src_delta = SourceDelta { text: new_source };
569        let scene_graph_delta = SceneGraphDelta {
570            new_graph: self.scene_graph_for_ui(),
571            invalidates_ids: false,
572            new_objects: vec![sketch_id],
573            exec_outcome: outcome,
574        };
575        Ok((src_delta, scene_graph_delta, sketch_id))
576    }
577
578    async fn edit_sketch(
579        &mut self,
580        ctx: &ExecutorContext,
581        _project: ProjectId,
582        _file: FileId,
583        _version: Version,
584        sketch: ObjectId,
585    ) -> ExecResult<SceneGraphDelta> {
586        // TODO: Check version.
587
588        // Look up existing sketch.
589        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
590            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
591        })?;
592        let ObjectKind::Sketch(_) = &sketch_object.kind else {
593            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
594                "Object is not a sketch, it is {}",
595                sketch_object.kind.human_friendly_kind_with_article()
596            ))));
597        };
598        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
599
600        // Enter sketch mode by setting the sketch_mode.
601        self.scene_graph.sketch_mode = Some(sketch);
602
603        // Truncate after the sketch block for mock execution.
604        let mut truncated_program = self.program.clone();
605        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
606            .map_err(KclErrorWithOutputs::no_outputs)?;
607
608        // Execute in mock mode to ensure state is up to date. The caller will
609        // want freedom analysis to display segments correctly.
610        let outcome = ctx
611            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
612            .await?;
613
614        // MockConfig::default() has freedom_analysis: true
615        let outcome = self.update_state_after_exec(outcome, true);
616        let scene_graph_delta = SceneGraphDelta {
617            new_graph: self.scene_graph_for_ui(),
618            invalidates_ids: false,
619            new_objects: Vec::new(),
620            exec_outcome: outcome,
621        };
622        Ok(scene_graph_delta)
623    }
624
625    async fn exit_sketch(
626        &mut self,
627        ctx: &ExecutorContext,
628        _version: Version,
629        sketch: ObjectId,
630    ) -> ExecResult<SceneGraph> {
631        // TODO: Check version.
632        #[cfg(not(target_arch = "wasm32"))]
633        let _ = sketch;
634        #[cfg(target_arch = "wasm32")]
635        if self.scene_graph.sketch_mode != Some(sketch) {
636            web_sys::console::warn_1(
637                &format!(
638                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
639                    &self.scene_graph.sketch_mode
640                )
641                .into(),
642            );
643        }
644        self.scene_graph.sketch_mode = None;
645
646        // Execute.
647        let outcome = ctx.run_with_caching(self.program.clone()).await?;
648
649        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
650        self.update_state_after_exec(outcome, false);
651
652        Ok(self.scene_graph_for_ui())
653    }
654
655    async fn delete_sketch(
656        &mut self,
657        ctx: &ExecutorContext,
658        _version: Version,
659        sketch: ObjectId,
660    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
661        // TODO: Check version.
662
663        let mut new_ast = self.program.ast.clone();
664
665        // Look up existing sketch.
666        let sketch_id = sketch;
667        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
668            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
669        })?;
670        let ObjectKind::Sketch(_) = &sketch_object.kind else {
671            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
672                "Object is not a sketch, it is {}",
673                sketch_object.kind.human_friendly_kind_with_article(),
674            ))));
675        };
676
677        // Modify the AST to remove the sketch.
678        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
679            .map_err(KclErrorWithOutputs::no_outputs)?;
680
681        self.execute_after_delete_sketch(ctx, &mut new_ast).await
682    }
683
684    async fn add_segment(
685        &mut self,
686        ctx: &ExecutorContext,
687        _version: Version,
688        sketch: ObjectId,
689        segment: SegmentCtor,
690        _label: Option<String>,
691    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
692        // TODO: Check version.
693        match segment {
694            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
695            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
696            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
697            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
698            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
699        }
700    }
701
702    async fn edit_segments(
703        &mut self,
704        ctx: &ExecutorContext,
705        _version: Version,
706        sketch: ObjectId,
707        segments: Vec<ExistingSegmentCtor>,
708    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
709        // TODO: Check version.
710        let sketch_block_ref =
711            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
712
713        let mut new_ast = self.program.ast.clone();
714        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
715        let mut invalidates_ids = false;
716
717        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
718        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
719        for segment in &segments {
720            segment_ids_edited.insert(segment.id);
721            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
722                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
723                && let ObjectKind::Segment {
724                    segment: Segment::ControlPointSpline(existing_spline),
725                } = &existing_object.kind
726                && existing_spline.controls.len() != new_ctor.points.len()
727            {
728                invalidates_ids = true;
729            }
730        }
731
732        // Preprocess segments into a final_edits vector to handle if segments contains:
733        // - edit start point of line1 (as SegmentCtor::Point)
734        // - edit end point of line1 (as SegmentCtor::Point)
735        //
736        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
737        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
738        //
739        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
740        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
741        // so the above example would result in a single line1 edit:
742        // - the first start point edit creates a new line edit entry in final_edits
743        // - the second end point edit finds this line edit and mutates the end position only.
744        //
745        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
746        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
747
748        for segment in segments {
749            let segment_id = segment.id;
750            match segment.ctor {
751                SegmentCtor::Point(ctor) => {
752                    // Find the owner, if any (point -> line / arc)
753                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
754                        && let ObjectKind::Segment { segment } = &segment_object.kind
755                        && let Segment::Point(point) = segment
756                        && let Some(owner_id) = point.owner
757                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
758                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
759                    {
760                        match owner_segment {
761                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
762                                if let Some(existing) = final_edits.get_mut(&owner_id) {
763                                    let SegmentCtor::Line(line_ctor) = existing else {
764                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
765                                            "Internal: Expected line ctor for owner, but found {}",
766                                            existing.human_friendly_kind_with_article()
767                                        ))));
768                                    };
769                                    // Line owner is already in final_edits -> apply this point edit
770                                    if line.start == segment_id {
771                                        line_ctor.start = ctor.position;
772                                    } else {
773                                        line_ctor.end = ctor.position;
774                                    }
775                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
776                                    // Line owner is not in final_edits yet -> create it
777                                    let mut line_ctor = line_ctor.clone();
778                                    if line.start == segment_id {
779                                        line_ctor.start = ctor.position;
780                                    } else {
781                                        line_ctor.end = ctor.position;
782                                    }
783                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
784                                } else {
785                                    // This should never run..
786                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
787                                        "Internal: Line does not have line ctor, but found {}",
788                                        line.ctor.human_friendly_kind_with_article()
789                                    ))));
790                                }
791                                continue;
792                            }
793                            Segment::Arc(arc)
794                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
795                            {
796                                if let Some(existing) = final_edits.get_mut(&owner_id) {
797                                    let SegmentCtor::Arc(arc_ctor) = existing else {
798                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
799                                            "Internal: Expected arc ctor for owner, but found {}",
800                                            existing.human_friendly_kind_with_article()
801                                        ))));
802                                    };
803                                    if arc.start == segment_id {
804                                        arc_ctor.start = ctor.position;
805                                    } else if arc.end == segment_id {
806                                        arc_ctor.end = ctor.position;
807                                    } else {
808                                        arc_ctor.center = ctor.position;
809                                    }
810                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
811                                    let mut arc_ctor = arc_ctor.clone();
812                                    if arc.start == segment_id {
813                                        arc_ctor.start = ctor.position;
814                                    } else if arc.end == segment_id {
815                                        arc_ctor.end = ctor.position;
816                                    } else {
817                                        arc_ctor.center = ctor.position;
818                                    }
819                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
820                                } else {
821                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
822                                        "Internal: Arc does not have arc ctor, but found {}",
823                                        arc.ctor.human_friendly_kind_with_article()
824                                    ))));
825                                }
826                                continue;
827                            }
828                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
829                                if let Some(existing) = final_edits.get_mut(&owner_id) {
830                                    let SegmentCtor::Circle(circle_ctor) = existing else {
831                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
832                                            "Internal: Expected circle ctor for owner, but found {}",
833                                            existing.human_friendly_kind_with_article()
834                                        ))));
835                                    };
836                                    if circle.start == segment_id {
837                                        circle_ctor.start = ctor.position;
838                                    } else {
839                                        circle_ctor.center = ctor.position;
840                                    }
841                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
842                                    let mut circle_ctor = circle_ctor.clone();
843                                    if circle.start == segment_id {
844                                        circle_ctor.start = ctor.position;
845                                    } else {
846                                        circle_ctor.center = ctor.position;
847                                    }
848                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
849                                } else {
850                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851                                        "Internal: Circle does not have circle ctor, but found {}",
852                                        circle.ctor.human_friendly_kind_with_article()
853                                    ))));
854                                }
855                                continue;
856                            }
857                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
858                                let Some(control_index) =
859                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
860                                else {
861                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
862                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
863                                    ))));
864                                };
865                                if let Some(existing) = final_edits.get_mut(&owner_id) {
866                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
867                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
868                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
869                                            existing.human_friendly_kind_with_article()
870                                        ))));
871                                    };
872                                    spline_ctor.points[control_index] = ctor.position;
873                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
874                                    let mut spline_ctor = spline_ctor.clone();
875                                    spline_ctor.points[control_index] = ctor.position;
876                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
877                                } else {
878                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
879                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
880                                        spline.ctor.human_friendly_kind_with_article()
881                                    ))));
882                                }
883                                continue;
884                            }
885                            _ => {}
886                        }
887                    }
888
889                    // No owner, it's an individual point
890                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
891                }
892                SegmentCtor::Line(ctor) => {
893                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
894                }
895                SegmentCtor::Arc(ctor) => {
896                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
897                }
898                SegmentCtor::Circle(ctor) => {
899                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
900                }
901                SegmentCtor::ControlPointSpline(ctor) => {
902                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
903                }
904            }
905        }
906
907        for (segment_id, ctor) in final_edits {
908            match ctor {
909                SegmentCtor::Point(ctor) => self
910                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
911                    .map_err(KclErrorWithOutputs::no_outputs)?,
912                SegmentCtor::Line(ctor) => self
913                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
914                    .map_err(KclErrorWithOutputs::no_outputs)?,
915                SegmentCtor::Arc(ctor) => self
916                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
917                    .map_err(KclErrorWithOutputs::no_outputs)?,
918                SegmentCtor::Circle(ctor) => self
919                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
920                    .map_err(KclErrorWithOutputs::no_outputs)?,
921                SegmentCtor::ControlPointSpline(ctor) => self
922                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
923                    .map_err(KclErrorWithOutputs::no_outputs)?,
924            }
925        }
926        let (source_delta, mut scene_graph_delta) = self
927            .execute_after_edit(
928                ctx,
929                sketch,
930                sketch_block_ref,
931                segment_ids_edited,
932                EditDeleteKind::Edit,
933                &mut new_ast,
934            )
935            .await?;
936        if invalidates_ids {
937            scene_graph_delta.invalidates_ids = true;
938        }
939        Ok((source_delta, scene_graph_delta))
940    }
941
942    async fn delete_objects(
943        &mut self,
944        ctx: &ExecutorContext,
945        _version: Version,
946        sketch: ObjectId,
947        constraint_ids: Vec<ObjectId>,
948        segment_ids: Vec<ObjectId>,
949    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
950        // TODO: Check version.
951        let sketch_block_ref =
952            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
953
954        // Deduplicate IDs.
955        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
956        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
957
958        // If a point is owned by a Line/Arc, we want to delete the owner, which will
959        // also delete the point, as well as other points that are owned by the owner.
960        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
961
962        for segment_id in segment_ids_set.iter().copied() {
963            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
964                let ObjectKind::Segment { segment } = &segment_object.kind else {
965                    return None;
966                };
967                match segment {
968                    Segment::Point(point) => point.owner,
969                    Segment::Line(line) => line.owner,
970                    _ => None,
971                }
972            });
973
974            if let Some(owner_id) = owner_id
975                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
976                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
977                && matches!(
978                    owner_segment,
979                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
980                )
981            {
982                // segment is owned -> delete the owner
983                resolved_segment_ids_to_delete.insert(owner_id);
984            } else {
985                // segment is not owned by anything -> can be deleted
986                resolved_segment_ids_to_delete.insert(segment_id);
987            }
988        }
989        let referenced_constraint_ids = self
990            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
991            .map_err(KclErrorWithOutputs::no_outputs)?;
992
993        let mut new_ast = self.program.ast.clone();
994
995        for constraint_id in referenced_constraint_ids {
996            if constraint_ids_set.contains(&constraint_id) {
997                continue;
998            }
999
1000            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1001                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1002            })?;
1003            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1004                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1005                    "Object is not a constraint, it is {}",
1006                    constraint_object.kind.human_friendly_kind_with_article()
1007                ))));
1008            };
1009
1010            match constraint {
1011                Constraint::Coincident(coincident) => {
1012                    let remaining_segments =
1013                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1014
1015                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1016                    if remaining_segments.len() >= 2 {
1017                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1018                            .map_err(KclErrorWithOutputs::no_outputs)?;
1019                    } else {
1020                        constraint_ids_set.insert(constraint_id);
1021                    }
1022                }
1023                Constraint::EqualRadius(equal_radius) => {
1024                    let remaining_input = equal_radius
1025                        .input
1026                        .iter()
1027                        .copied()
1028                        .filter(|segment_id| {
1029                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1030                        })
1031                        .collect::<Vec<_>>();
1032
1033                    if remaining_input.len() >= 2 {
1034                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1035                            .map_err(KclErrorWithOutputs::no_outputs)?;
1036                    } else {
1037                        constraint_ids_set.insert(constraint_id);
1038                    }
1039                }
1040                Constraint::LinesEqualLength(lines_equal_length) => {
1041                    let remaining_lines = lines_equal_length
1042                        .lines
1043                        .iter()
1044                        .copied()
1045                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1046                        .collect::<Vec<_>>();
1047
1048                    // Equal length constraint is only valid with at least 2 lines
1049                    if remaining_lines.len() >= 2 {
1050                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1051                            .map_err(KclErrorWithOutputs::no_outputs)?;
1052                    } else {
1053                        constraint_ids_set.insert(constraint_id);
1054                    }
1055                }
1056                Constraint::Parallel(parallel) => {
1057                    let remaining_lines = parallel
1058                        .lines
1059                        .iter()
1060                        .copied()
1061                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1062                        .collect::<Vec<_>>();
1063
1064                    if remaining_lines.len() >= 2 {
1065                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1066                            .map_err(KclErrorWithOutputs::no_outputs)?;
1067                    } else {
1068                        constraint_ids_set.insert(constraint_id);
1069                    }
1070                }
1071                Constraint::Horizontal(Horizontal::Points { points }) => {
1072                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1073
1074                    if remaining_points.len() >= 2 {
1075                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1076                            .map_err(KclErrorWithOutputs::no_outputs)?;
1077                    } else {
1078                        constraint_ids_set.insert(constraint_id);
1079                    }
1080                }
1081                Constraint::Vertical(Vertical::Points { points }) => {
1082                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1083
1084                    if remaining_points.len() >= 2 {
1085                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1086                            .map_err(KclErrorWithOutputs::no_outputs)?;
1087                    } else {
1088                        constraint_ids_set.insert(constraint_id);
1089                    }
1090                }
1091                Constraint::Fixed(fixed) => {
1092                    if fixed.points.iter().any(|fixed_point| {
1093                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1094                    }) {
1095                        constraint_ids_set.insert(constraint_id);
1096                    }
1097                }
1098                _ => {
1099                    // All other constraint types: if referenced by a segment -> delete the constraint
1100                    constraint_ids_set.insert(constraint_id);
1101                }
1102            }
1103        }
1104
1105        for constraint_id in constraint_ids_set {
1106            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1107                .map_err(KclErrorWithOutputs::no_outputs)?;
1108        }
1109        for segment_id in resolved_segment_ids_to_delete {
1110            self.delete_segment(&mut new_ast, sketch, segment_id)
1111                .map_err(KclErrorWithOutputs::no_outputs)?;
1112        }
1113
1114        self.execute_after_edit(
1115            ctx,
1116            sketch,
1117            sketch_block_ref,
1118            Default::default(),
1119            EditDeleteKind::DeleteNonSketch,
1120            &mut new_ast,
1121        )
1122        .await
1123    }
1124
1125    async fn add_constraint(
1126        &mut self,
1127        ctx: &ExecutorContext,
1128        _version: Version,
1129        sketch: ObjectId,
1130        constraint: Constraint,
1131    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1132        // TODO: Check version.
1133
1134        // Save the original state as a backup - we'll restore it if anything fails
1135        let original_program = self.program.clone();
1136        let original_scene_graph = self.scene_graph.clone();
1137
1138        let mut new_ast = self.program.ast.clone();
1139        let sketch_block_ref = match constraint {
1140            Constraint::Coincident(coincident) => self
1141                .add_coincident(sketch, coincident, &mut new_ast)
1142                .await
1143                .map_err(KclErrorWithOutputs::no_outputs)?,
1144            Constraint::Distance(distance) => self
1145                .add_distance(sketch, distance, &mut new_ast)
1146                .await
1147                .map_err(KclErrorWithOutputs::no_outputs)?,
1148            Constraint::EqualRadius(equal_radius) => self
1149                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1150                .await
1151                .map_err(KclErrorWithOutputs::no_outputs)?,
1152            Constraint::Fixed(fixed) => self
1153                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1154                .await
1155                .map_err(KclErrorWithOutputs::no_outputs)?,
1156            Constraint::HorizontalDistance(distance) => self
1157                .add_horizontal_distance(sketch, distance, &mut new_ast)
1158                .await
1159                .map_err(KclErrorWithOutputs::no_outputs)?,
1160            Constraint::VerticalDistance(distance) => self
1161                .add_vertical_distance(sketch, distance, &mut new_ast)
1162                .await
1163                .map_err(KclErrorWithOutputs::no_outputs)?,
1164            Constraint::Horizontal(horizontal) => self
1165                .add_horizontal(sketch, horizontal, &mut new_ast)
1166                .await
1167                .map_err(KclErrorWithOutputs::no_outputs)?,
1168            Constraint::LinesEqualLength(lines_equal_length) => self
1169                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1170                .await
1171                .map_err(KclErrorWithOutputs::no_outputs)?,
1172            Constraint::Midpoint(midpoint) => self
1173                .add_midpoint(sketch, midpoint, &mut new_ast)
1174                .await
1175                .map_err(KclErrorWithOutputs::no_outputs)?,
1176            Constraint::Parallel(parallel) => self
1177                .add_parallel(sketch, parallel, &mut new_ast)
1178                .await
1179                .map_err(KclErrorWithOutputs::no_outputs)?,
1180            Constraint::Perpendicular(perpendicular) => self
1181                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1182                .await
1183                .map_err(KclErrorWithOutputs::no_outputs)?,
1184            Constraint::Radius(radius) => self
1185                .add_radius(sketch, radius, &mut new_ast)
1186                .await
1187                .map_err(KclErrorWithOutputs::no_outputs)?,
1188            Constraint::Diameter(diameter) => self
1189                .add_diameter(sketch, diameter, &mut new_ast)
1190                .await
1191                .map_err(KclErrorWithOutputs::no_outputs)?,
1192            Constraint::Symmetric(symmetric) => self
1193                .add_symmetric(sketch, symmetric, &mut new_ast)
1194                .await
1195                .map_err(KclErrorWithOutputs::no_outputs)?,
1196            Constraint::Vertical(vertical) => self
1197                .add_vertical(sketch, vertical, &mut new_ast)
1198                .await
1199                .map_err(KclErrorWithOutputs::no_outputs)?,
1200            Constraint::Angle(lines_at_angle) => self
1201                .add_angle(sketch, lines_at_angle, &mut new_ast)
1202                .await
1203                .map_err(KclErrorWithOutputs::no_outputs)?,
1204            Constraint::Tangent(tangent) => self
1205                .add_tangent(sketch, tangent, &mut new_ast)
1206                .await
1207                .map_err(KclErrorWithOutputs::no_outputs)?,
1208        };
1209
1210        let result = self
1211            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1212            .await;
1213
1214        // If execution failed, restore the original state to prevent corruption
1215        if result.is_err() {
1216            self.program = original_program;
1217            self.scene_graph = original_scene_graph;
1218        }
1219
1220        result
1221    }
1222
1223    async fn chain_segment(
1224        &mut self,
1225        ctx: &ExecutorContext,
1226        version: Version,
1227        sketch: ObjectId,
1228        previous_segment_end_point_id: ObjectId,
1229        segment: SegmentCtor,
1230        _label: Option<String>,
1231    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1232        // TODO: Check version.
1233
1234        // First, add the segment (line) to get its start point ID
1235        let SegmentCtor::Line(line_ctor) = segment else {
1236            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1237                "chain_segment currently only supports Line segments, got {}",
1238                segment.human_friendly_kind_with_article(),
1239            ))));
1240        };
1241
1242        // Add the line segment first - this updates self.program and self.scene_graph
1243        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1244
1245        // Find the new line's start point ID from the updated scene graph
1246        // add_line updates self.scene_graph, so we can use that
1247        let new_line_id = first_scene_delta
1248            .new_objects
1249            .iter()
1250            .find(|&obj_id| {
1251                let obj = self.scene_graph.objects.get(obj_id.0);
1252                if let Some(obj) = obj {
1253                    matches!(
1254                        &obj.kind,
1255                        ObjectKind::Segment {
1256                            segment: Segment::Line(_)
1257                        }
1258                    )
1259                } else {
1260                    false
1261                }
1262            })
1263            .ok_or_else(|| {
1264                KclErrorWithOutputs::no_outputs(KclError::refactor(
1265                    "Failed to find new line segment in scene graph".to_string(),
1266                ))
1267            })?;
1268
1269        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1270            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1271                "New line object not found: {new_line_id:?}"
1272            )))
1273        })?;
1274
1275        let ObjectKind::Segment {
1276            segment: new_line_segment,
1277        } = &new_line_obj.kind
1278        else {
1279            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1280                "Object is not a segment: {new_line_obj:?}"
1281            ))));
1282        };
1283
1284        let Segment::Line(new_line) = new_line_segment else {
1285            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1286                "Segment is not a line: {new_line_segment:?}"
1287            ))));
1288        };
1289
1290        let new_line_start_point_id = new_line.start;
1291
1292        // Now add the coincident constraint between the previous end point and the new line's start point.
1293        let coincident = Coincident {
1294            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1295        };
1296
1297        let (final_src_delta, final_scene_delta) = self
1298            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1299            .await?;
1300
1301        // Combine new objects from the line addition and the constraint addition.
1302        // Both add_line and add_constraint now populate new_objects correctly.
1303        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1304        combined_new_objects.extend(final_scene_delta.new_objects);
1305
1306        let scene_graph_delta = SceneGraphDelta {
1307            new_graph: self.scene_graph_for_ui(),
1308            invalidates_ids: false,
1309            new_objects: combined_new_objects,
1310            exec_outcome: final_scene_delta.exec_outcome,
1311        };
1312
1313        Ok((final_src_delta, scene_graph_delta))
1314    }
1315
1316    async fn edit_constraint(
1317        &mut self,
1318        ctx: &ExecutorContext,
1319        _version: Version,
1320        sketch: ObjectId,
1321        constraint_id: ObjectId,
1322        value_expression: String,
1323    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1324        // TODO: Check version.
1325        let sketch_block_ref =
1326            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1327
1328        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1329            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1330        })?;
1331        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1332            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1333                "Object is not a constraint: {constraint_id:?}"
1334            ))));
1335        }
1336
1337        let mut new_ast = self.program.ast.clone();
1338
1339        // Parse the expression string into an AST node.
1340        let (parsed, errors) = Program::parse(&value_expression)
1341            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1342        if !errors.is_empty() {
1343            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1344                "Error parsing value expression: {errors:?}"
1345            ))));
1346        }
1347        let mut parsed = parsed.ok_or_else(|| {
1348            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1349        })?;
1350        if parsed.ast.body.is_empty() {
1351            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1352                "Empty value expression".to_string(),
1353            )));
1354        }
1355        let first = parsed.ast.body.remove(0);
1356        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1357            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1358                "Value expression must be a simple expression".to_string(),
1359            )));
1360        };
1361
1362        let new_value: ast::BinaryPart = expr_stmt
1363            .inner
1364            .expression
1365            .try_into()
1366            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1367
1368        self.mutate_ast(
1369            &mut new_ast,
1370            constraint_id,
1371            AstMutateCommand::EditConstraintValue { value: new_value },
1372        )
1373        .map_err(KclErrorWithOutputs::no_outputs)?;
1374
1375        self.execute_after_edit(
1376            ctx,
1377            sketch,
1378            sketch_block_ref,
1379            Default::default(),
1380            EditDeleteKind::Edit,
1381            &mut new_ast,
1382        )
1383        .await
1384    }
1385
1386    async fn edit_distance_constraint_label_position(
1387        &mut self,
1388        ctx: &ExecutorContext,
1389        _version: Version,
1390        sketch: ObjectId,
1391        constraint_id: ObjectId,
1392        label_position: Point2d<Number>,
1393        anchor_segment_ids: Vec<ObjectId>,
1394    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1395        // TODO: Check version.
1396        let sketch_block_ref =
1397            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1398
1399        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1400            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1401        })?;
1402        if !matches!(
1403            &object.kind,
1404            ObjectKind::Constraint {
1405                constraint: Constraint::Distance(_)
1406                    | Constraint::HorizontalDistance(_)
1407                    | Constraint::VerticalDistance(_)
1408                    | Constraint::Radius(_)
1409                    | Constraint::Diameter(_),
1410            }
1411        ) {
1412            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1413                "Object does not support labelPosition: {constraint_id:?}"
1414            ))));
1415        }
1416
1417        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1418            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1419                "Could not convert label position to AST: {err}"
1420            )))
1421        })?;
1422        let mut new_ast = self.program.ast.clone();
1423        self.mutate_ast(
1424            &mut new_ast,
1425            constraint_id,
1426            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1427        )
1428        .map_err(KclErrorWithOutputs::no_outputs)?;
1429
1430        self.execute_after_edit(
1431            ctx,
1432            sketch,
1433            sketch_block_ref,
1434            anchor_segment_ids.into_iter().collect(),
1435            EditDeleteKind::Edit,
1436            &mut new_ast,
1437        )
1438        .await
1439    }
1440
1441    /// Splitting a segment means creating a new segment, editing the old one, and then
1442    /// migrating a bunch of the constraints from the original segment to the new one
1443    /// (i.e. deleting them and re-adding them on the other segment).
1444    ///
1445    /// To keep this efficient we require as few executions as possible: we create the
1446    /// new segment first (to get its id), then do all edits and new constraints, and
1447    /// do all deletes at the end (since deletes invalidate ids).
1448    async fn batch_split_segment_operations(
1449        &mut self,
1450        ctx: &ExecutorContext,
1451        _version: Version,
1452        sketch: ObjectId,
1453        edit_segments: Vec<ExistingSegmentCtor>,
1454        add_constraints: Vec<Constraint>,
1455        delete_constraint_ids: Vec<ObjectId>,
1456        _new_segment_info: sketch::NewSegmentInfo,
1457    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1458        // TODO: Check version.
1459        let sketch_block_ref =
1460            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1461
1462        let mut new_ast = self.program.ast.clone();
1463        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1464
1465        // Step 1: Edit segments
1466        for segment in edit_segments {
1467            segment_ids_edited.insert(segment.id);
1468            match segment.ctor {
1469                SegmentCtor::Point(ctor) => self
1470                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1471                    .map_err(KclErrorWithOutputs::no_outputs)?,
1472                SegmentCtor::Line(ctor) => self
1473                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1474                    .map_err(KclErrorWithOutputs::no_outputs)?,
1475                SegmentCtor::Arc(ctor) => self
1476                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1477                    .map_err(KclErrorWithOutputs::no_outputs)?,
1478                SegmentCtor::Circle(ctor) => self
1479                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1480                    .map_err(KclErrorWithOutputs::no_outputs)?,
1481                SegmentCtor::ControlPointSpline(ctor) => self
1482                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1483                    .map_err(KclErrorWithOutputs::no_outputs)?,
1484            }
1485        }
1486
1487        // Step 2: Add all constraints
1488        for constraint in add_constraints {
1489            match constraint {
1490                Constraint::Coincident(coincident) => {
1491                    self.add_coincident(sketch, coincident, &mut new_ast)
1492                        .await
1493                        .map_err(KclErrorWithOutputs::no_outputs)?;
1494                }
1495                Constraint::Distance(distance) => {
1496                    self.add_distance(sketch, distance, &mut new_ast)
1497                        .await
1498                        .map_err(KclErrorWithOutputs::no_outputs)?;
1499                }
1500                Constraint::EqualRadius(equal_radius) => {
1501                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1502                        .await
1503                        .map_err(KclErrorWithOutputs::no_outputs)?;
1504                }
1505                Constraint::Fixed(fixed) => {
1506                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1507                        .await
1508                        .map_err(KclErrorWithOutputs::no_outputs)?;
1509                }
1510                Constraint::HorizontalDistance(distance) => {
1511                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1512                        .await
1513                        .map_err(KclErrorWithOutputs::no_outputs)?;
1514                }
1515                Constraint::VerticalDistance(distance) => {
1516                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1517                        .await
1518                        .map_err(KclErrorWithOutputs::no_outputs)?;
1519                }
1520                Constraint::Horizontal(horizontal) => {
1521                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1522                        .await
1523                        .map_err(KclErrorWithOutputs::no_outputs)?;
1524                }
1525                Constraint::LinesEqualLength(lines_equal_length) => {
1526                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1527                        .await
1528                        .map_err(KclErrorWithOutputs::no_outputs)?;
1529                }
1530                Constraint::Midpoint(midpoint) => {
1531                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1532                        .await
1533                        .map_err(KclErrorWithOutputs::no_outputs)?;
1534                }
1535                Constraint::Parallel(parallel) => {
1536                    self.add_parallel(sketch, parallel, &mut new_ast)
1537                        .await
1538                        .map_err(KclErrorWithOutputs::no_outputs)?;
1539                }
1540                Constraint::Perpendicular(perpendicular) => {
1541                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1542                        .await
1543                        .map_err(KclErrorWithOutputs::no_outputs)?;
1544                }
1545                Constraint::Vertical(vertical) => {
1546                    self.add_vertical(sketch, vertical, &mut new_ast)
1547                        .await
1548                        .map_err(KclErrorWithOutputs::no_outputs)?;
1549                }
1550                Constraint::Diameter(diameter) => {
1551                    self.add_diameter(sketch, diameter, &mut new_ast)
1552                        .await
1553                        .map_err(KclErrorWithOutputs::no_outputs)?;
1554                }
1555                Constraint::Radius(radius) => {
1556                    self.add_radius(sketch, radius, &mut new_ast)
1557                        .await
1558                        .map_err(KclErrorWithOutputs::no_outputs)?;
1559                }
1560                Constraint::Symmetric(symmetric) => {
1561                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1562                        .await
1563                        .map_err(KclErrorWithOutputs::no_outputs)?;
1564                }
1565                Constraint::Angle(angle) => {
1566                    self.add_angle(sketch, angle, &mut new_ast)
1567                        .await
1568                        .map_err(KclErrorWithOutputs::no_outputs)?;
1569                }
1570                Constraint::Tangent(tangent) => {
1571                    self.add_tangent(sketch, tangent, &mut new_ast)
1572                        .await
1573                        .map_err(KclErrorWithOutputs::no_outputs)?;
1574                }
1575            }
1576        }
1577
1578        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1579        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1580
1581        let has_constraint_deletions = !constraint_ids_set.is_empty();
1582        for constraint_id in constraint_ids_set {
1583            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1584                .map_err(KclErrorWithOutputs::no_outputs)?;
1585        }
1586
1587        // Step 4: Execute once at the end
1588        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1589        // But we'll manually set invalidates_ids: true if we deleted constraints
1590        let (source_delta, mut scene_graph_delta) = self
1591            .execute_after_edit(
1592                ctx,
1593                sketch,
1594                sketch_block_ref,
1595                segment_ids_edited,
1596                EditDeleteKind::Edit,
1597                &mut new_ast,
1598            )
1599            .await?;
1600
1601        // If we deleted constraints, set invalidates_ids: true
1602        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1603        if has_constraint_deletions {
1604            scene_graph_delta.invalidates_ids = true;
1605        }
1606
1607        Ok((source_delta, scene_graph_delta))
1608    }
1609
1610    async fn batch_tail_cut_operations(
1611        &mut self,
1612        ctx: &ExecutorContext,
1613        _version: Version,
1614        sketch: ObjectId,
1615        edit_segments: Vec<ExistingSegmentCtor>,
1616        add_constraints: Vec<Constraint>,
1617        delete_constraint_ids: Vec<ObjectId>,
1618        additional_edited_segment_ids: Vec<ObjectId>,
1619    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1620        let sketch_block_ref =
1621            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1622
1623        let mut new_ast = self.program.ast.clone();
1624        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1625
1626        // Step 1: Edit segments (usually a single segment for tail cut)
1627        for segment in edit_segments {
1628            segment_ids_edited.insert(segment.id);
1629            match segment.ctor {
1630                SegmentCtor::Point(ctor) => self
1631                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1632                    .map_err(KclErrorWithOutputs::no_outputs)?,
1633                SegmentCtor::Line(ctor) => self
1634                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1635                    .map_err(KclErrorWithOutputs::no_outputs)?,
1636                SegmentCtor::Arc(ctor) => self
1637                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1638                    .map_err(KclErrorWithOutputs::no_outputs)?,
1639                SegmentCtor::Circle(ctor) => self
1640                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1641                    .map_err(KclErrorWithOutputs::no_outputs)?,
1642                SegmentCtor::ControlPointSpline(ctor) => self
1643                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1644                    .map_err(KclErrorWithOutputs::no_outputs)?,
1645            }
1646        }
1647
1648        segment_ids_edited.extend(additional_edited_segment_ids);
1649
1650        // Step 2: Add coincident constraints
1651        for constraint in add_constraints {
1652            match constraint {
1653                Constraint::Coincident(coincident) => {
1654                    self.add_coincident(sketch, coincident, &mut new_ast)
1655                        .await
1656                        .map_err(KclErrorWithOutputs::no_outputs)?;
1657                }
1658                other => {
1659                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1660                        "unsupported constraint in tail cut batch: {other:?}"
1661                    ))));
1662                }
1663            }
1664        }
1665
1666        // Step 3: Delete constraints (if any)
1667        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1668
1669        let has_constraint_deletions = !constraint_ids_set.is_empty();
1670        for constraint_id in constraint_ids_set {
1671            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1672                .map_err(KclErrorWithOutputs::no_outputs)?;
1673        }
1674
1675        // Step 4: Single execute_after_edit
1676        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1677        // But we'll manually set invalidates_ids: true if we deleted constraints
1678        let (source_delta, mut scene_graph_delta) = self
1679            .execute_after_edit(
1680                ctx,
1681                sketch,
1682                sketch_block_ref,
1683                segment_ids_edited,
1684                EditDeleteKind::Edit,
1685                &mut new_ast,
1686            )
1687            .await?;
1688
1689        // If we deleted constraints, set invalidates_ids: true
1690        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1691        if has_constraint_deletions {
1692            scene_graph_delta.invalidates_ids = true;
1693        }
1694
1695        Ok((source_delta, scene_graph_delta))
1696    }
1697}
1698
1699impl FrontendState {
1700    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1701        self.program = program.clone();
1702
1703        // Execute so that the objects are updated and available for the next
1704        // API call.
1705        // This always uses engine execution (not mock) so that things are cached.
1706        // Engine execution now runs freedom analysis automatically.
1707        // Keep existing checkpoints alive here. History may still reference
1708        // older committed sketch states across a direct-edit boundary, and a
1709        // checkpoint restore is a full state replacement anyway. We append a
1710        // fresh baseline checkpoint after the full execution below.
1711        // Clear the freedom cache since IDs might have changed after direct editing
1712        // and we're about to run freedom analysis which will repopulate it.
1713        self.point_freedom_cache.clear();
1714        match ctx.run_with_caching(program).await {
1715            Ok(outcome) => {
1716                let outcome = self.update_state_after_exec(outcome, true);
1717                let checkpoint_id = self
1718                    .create_sketch_checkpoint(outcome.clone())
1719                    .await
1720                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1721                Ok(SetProgramOutcome::Success {
1722                    scene_graph: Box::new(self.scene_graph_for_ui()),
1723                    exec_outcome: Box::new(outcome),
1724                    checkpoint_id: Some(checkpoint_id),
1725                })
1726            }
1727            Err(mut err) => {
1728                // Don't return an error just because execution failed. Instead,
1729                // update state as much as possible.
1730                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1731                self.update_state_after_exec(outcome, true);
1732                err.scene_graph = Some(self.scene_graph_for_ui());
1733                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1734            }
1735        }
1736    }
1737
1738    /// Decorate engine execution such that our state is updated and the scene
1739    /// graph is added to the return.
1740    pub async fn engine_execute(
1741        &mut self,
1742        ctx: &ExecutorContext,
1743        program: Program,
1744    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1745        self.program = program.clone();
1746
1747        // Engine execution now runs freedom analysis automatically. Clear the
1748        // freedom cache since IDs might have changed after direct editing, and
1749        // we're about to run freedom analysis which will repopulate it.
1750        self.point_freedom_cache.clear();
1751        match ctx.run_with_caching(program).await {
1752            Ok(outcome) => {
1753                let outcome = self.update_state_after_exec(outcome, true);
1754                Ok(SceneGraphDelta {
1755                    new_graph: self.scene_graph_for_ui(),
1756                    exec_outcome: outcome,
1757                    // We don't know what the new objects are.
1758                    new_objects: Default::default(),
1759                    // We don't know if IDs were invalidated.
1760                    invalidates_ids: Default::default(),
1761                })
1762            }
1763            Err(mut err) => {
1764                // Update state as much as possible, even when there's an error.
1765                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1766                self.update_state_after_exec(outcome, true);
1767                err.scene_graph = Some(self.scene_graph_for_ui());
1768                Err(err)
1769            }
1770        }
1771    }
1772
1773    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1774        if matches!(err.error, KclError::EngineHangup { .. }) {
1775            // It's not ideal to special-case this, but this error is very
1776            // common during development, and it causes confusing downstream
1777            // errors that have nothing to do with the actual problem.
1778            return Err(err);
1779        }
1780
1781        let KclErrorWithOutputs {
1782            error,
1783            mut non_fatal,
1784            variables,
1785            operations,
1786            artifact_graph,
1787            scene_objects,
1788            source_range_to_object,
1789            var_solutions,
1790            filenames,
1791            default_planes,
1792            ..
1793        } = err;
1794
1795        if let Some(source_range) = error.source_ranges().first() {
1796            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1797        } else {
1798            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1799        }
1800
1801        Ok(ExecOutcome {
1802            variables,
1803            filenames,
1804            operations,
1805            artifact_graph,
1806            scene_objects,
1807            source_range_to_object,
1808            var_solutions,
1809            issues: non_fatal,
1810            default_planes,
1811        })
1812    }
1813
1814    async fn add_point(
1815        &mut self,
1816        ctx: &ExecutorContext,
1817        sketch: ObjectId,
1818        ctor: PointCtor,
1819    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1820        // Create updated KCL source from args.
1821        let at_ast = to_ast_point2d(&ctor.position)
1822            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1823        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1824            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1825            unlabeled: None,
1826            arguments: vec![ast::LabeledArg {
1827                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1828                arg: at_ast,
1829            }],
1830            digest: None,
1831            non_code_meta: Default::default(),
1832        })));
1833
1834        // Look up existing sketch.
1835        let sketch_id = sketch;
1836        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1837            #[cfg(target_arch = "wasm32")]
1838            web_sys::console::error_1(
1839                &format!(
1840                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1841                    &self.scene_graph.objects
1842                )
1843                .into(),
1844            );
1845            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1846        })?;
1847        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1848            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1849                "Object is not a sketch, it is {}",
1850                sketch_object.kind.human_friendly_kind_with_article(),
1851            ))));
1852        };
1853        // Add the point to the AST of the sketch block.
1854        let mut new_ast = self.program.ast.clone();
1855        let (sketch_block_ref, _) = self
1856            .mutate_ast(
1857                &mut new_ast,
1858                sketch_id,
1859                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1860            )
1861            .map_err(KclErrorWithOutputs::no_outputs)?;
1862        // Convert to string source to create real source ranges.
1863        let new_source = source_from_ast(&new_ast);
1864        // Parse the new KCL source.
1865        let (new_program, errors) = Program::parse(&new_source)
1866            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1867        if !errors.is_empty() {
1868            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1869                "Error parsing KCL source after adding point: {errors:?}"
1870            ))));
1871        }
1872        let Some(new_program) = new_program else {
1873            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1874                "No AST produced after adding point".to_string(),
1875            )));
1876        };
1877
1878        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1879            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1880                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1881            )))
1882        })?;
1883
1884        // Make sure to only set this if there are no errors.
1885        self.program = new_program.clone();
1886
1887        // Truncate after the sketch block for mock execution.
1888        let mut truncated_program = new_program;
1889        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1890            .map_err(KclErrorWithOutputs::no_outputs)?;
1891
1892        // Execute.
1893        let outcome = ctx
1894            .run_mock(
1895                &truncated_program,
1896                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1897            )
1898            .await?;
1899
1900        let new_object_ids = {
1901            let make_err =
1902                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1903            let segment_id = outcome
1904                .source_range_to_object
1905                .get(&point_node_ref.range)
1906                .copied()
1907                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1908            let segment_object = outcome
1909                .scene_objects
1910                .get(segment_id.0)
1911                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1912            let ObjectKind::Segment { segment } = &segment_object.kind else {
1913                return Err(make_err(format!(
1914                    "Object is not a segment, it is {}",
1915                    segment_object.kind.human_friendly_kind_with_article()
1916                )));
1917            };
1918            let Segment::Point(_) = segment else {
1919                return Err(make_err(format!(
1920                    "Segment is not a point, it is {}",
1921                    segment.human_friendly_kind_with_article()
1922                )));
1923            };
1924            vec![segment_id]
1925        };
1926        let src_delta = SourceDelta { text: new_source };
1927        // Uses .no_freedom_analysis() so freedom_analysis: false
1928        let outcome = self.update_state_after_exec(outcome, false);
1929        let scene_graph_delta = SceneGraphDelta {
1930            new_graph: self.scene_graph_for_ui(),
1931            invalidates_ids: false,
1932            new_objects: new_object_ids,
1933            exec_outcome: outcome,
1934        };
1935        Ok((src_delta, scene_graph_delta))
1936    }
1937
1938    async fn add_line(
1939        &mut self,
1940        ctx: &ExecutorContext,
1941        sketch: ObjectId,
1942        ctor: LineCtor,
1943    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1944        // Create updated KCL source from args.
1945        let start_ast = to_ast_point2d(&ctor.start)
1946            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1947        let end_ast = to_ast_point2d(&ctor.end)
1948            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1949        let mut arguments = vec![
1950            ast::LabeledArg {
1951                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1952                arg: start_ast,
1953            },
1954            ast::LabeledArg {
1955                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1956                arg: end_ast,
1957            },
1958        ];
1959        // Add construction kwarg if construction is Some(true)
1960        if ctor.construction == Some(true) {
1961            arguments.push(ast::LabeledArg {
1962                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1963                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1964                    value: ast::LiteralValue::Bool(true),
1965                    raw: "true".to_string(),
1966                    digest: None,
1967                }))),
1968            });
1969        }
1970        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1971            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1972            unlabeled: None,
1973            arguments,
1974            digest: None,
1975            non_code_meta: Default::default(),
1976        })));
1977
1978        // Look up existing sketch.
1979        let sketch_id = sketch;
1980        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1981            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1982        })?;
1983        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1984            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1985                "Object is not a sketch, it is {}",
1986                sketch_object.kind.human_friendly_kind_with_article(),
1987            ))));
1988        };
1989        // Add the line to the AST of the sketch block.
1990        let mut new_ast = self.program.ast.clone();
1991        let (sketch_block_ref, _) = self
1992            .mutate_ast(
1993                &mut new_ast,
1994                sketch_id,
1995                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1996            )
1997            .map_err(KclErrorWithOutputs::no_outputs)?;
1998        // Convert to string source to create real source ranges.
1999        let new_source = source_from_ast(&new_ast);
2000        // Parse the new KCL source.
2001        let (new_program, errors) = Program::parse(&new_source)
2002            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2003        if !errors.is_empty() {
2004            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2005                "Error parsing KCL source after adding line: {errors:?}"
2006            ))));
2007        }
2008        let Some(new_program) = new_program else {
2009            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2010                "No AST produced after adding line".to_string(),
2011            )));
2012        };
2013
2014        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2015            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2016                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2017            )))
2018        })?;
2019
2020        // Make sure to only set this if there are no errors.
2021        self.program = new_program.clone();
2022
2023        // Truncate after the sketch block for mock execution.
2024        let mut truncated_program = new_program;
2025        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2026            .map_err(KclErrorWithOutputs::no_outputs)?;
2027
2028        // Execute.
2029        let outcome = ctx
2030            .run_mock(
2031                &truncated_program,
2032                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2033            )
2034            .await?;
2035
2036        let new_object_ids = {
2037            let make_err =
2038                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2039            let segment_id = outcome
2040                .source_range_to_object
2041                .get(&line_node_ref.range)
2042                .copied()
2043                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2044            let segment_object = outcome
2045                .scene_object_by_id(segment_id)
2046                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2047            let ObjectKind::Segment { segment } = &segment_object.kind else {
2048                return Err(make_err(format!(
2049                    "Object is not a segment, it is {}",
2050                    segment_object.kind.human_friendly_kind_with_article()
2051                )));
2052            };
2053            let Segment::Line(line) = segment else {
2054                return Err(make_err(format!(
2055                    "Segment is not a line, it is {}",
2056                    segment.human_friendly_kind_with_article()
2057                )));
2058            };
2059            vec![line.start, line.end, segment_id]
2060        };
2061        let src_delta = SourceDelta { text: new_source };
2062        // Uses .no_freedom_analysis() so freedom_analysis: false
2063        let outcome = self.update_state_after_exec(outcome, false);
2064        let scene_graph_delta = SceneGraphDelta {
2065            new_graph: self.scene_graph_for_ui(),
2066            invalidates_ids: false,
2067            new_objects: new_object_ids,
2068            exec_outcome: outcome,
2069        };
2070        Ok((src_delta, scene_graph_delta))
2071    }
2072
2073    async fn add_arc(
2074        &mut self,
2075        ctx: &ExecutorContext,
2076        sketch: ObjectId,
2077        ctor: ArcCtor,
2078    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2079        // Create updated KCL source from args.
2080        let start_ast = to_ast_point2d(&ctor.start)
2081            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082        let end_ast = to_ast_point2d(&ctor.end)
2083            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084        let center_ast = to_ast_point2d(&ctor.center)
2085            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2086        let mut arguments = vec![
2087            ast::LabeledArg {
2088                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2089                arg: start_ast,
2090            },
2091            ast::LabeledArg {
2092                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2093                arg: end_ast,
2094            },
2095            ast::LabeledArg {
2096                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2097                arg: center_ast,
2098            },
2099        ];
2100        // Add construction kwarg if construction is Some(true)
2101        if ctor.construction == Some(true) {
2102            arguments.push(ast::LabeledArg {
2103                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2104                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2105                    value: ast::LiteralValue::Bool(true),
2106                    raw: "true".to_string(),
2107                    digest: None,
2108                }))),
2109            });
2110        }
2111        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2112            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2113            unlabeled: None,
2114            arguments,
2115            digest: None,
2116            non_code_meta: Default::default(),
2117        })));
2118
2119        // Look up existing sketch.
2120        let sketch_id = sketch;
2121        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2122            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2123        })?;
2124        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2125            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2126                "Object is not a sketch, it is {}",
2127                sketch_object.kind.human_friendly_kind_with_article(),
2128            ))));
2129        };
2130        // Add the arc to the AST of the sketch block.
2131        let mut new_ast = self.program.ast.clone();
2132        let (sketch_block_ref, _) = self
2133            .mutate_ast(
2134                &mut new_ast,
2135                sketch_id,
2136                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2137            )
2138            .map_err(KclErrorWithOutputs::no_outputs)?;
2139        // Convert to string source to create real source ranges.
2140        let new_source = source_from_ast(&new_ast);
2141        // Parse the new KCL source.
2142        let (new_program, errors) = Program::parse(&new_source)
2143            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2144        if !errors.is_empty() {
2145            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2146                "Error parsing KCL source after adding arc: {errors:?}"
2147            ))));
2148        }
2149        let Some(new_program) = new_program else {
2150            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2151                "No AST produced after adding arc".to_string(),
2152            )));
2153        };
2154
2155        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2156            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2157                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2158            )))
2159        })?;
2160
2161        // Make sure to only set this if there are no errors.
2162        self.program = new_program.clone();
2163
2164        // Truncate after the sketch block for mock execution.
2165        let mut truncated_program = new_program;
2166        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2167            .map_err(KclErrorWithOutputs::no_outputs)?;
2168
2169        // Execute.
2170        let outcome = ctx
2171            .run_mock(
2172                &truncated_program,
2173                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2174            )
2175            .await?;
2176
2177        let new_object_ids = {
2178            let make_err =
2179                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2180            let segment_id = outcome
2181                .source_range_to_object
2182                .get(&arc_node_ref.range)
2183                .copied()
2184                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2185            let segment_object = outcome
2186                .scene_objects
2187                .get(segment_id.0)
2188                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2189            let ObjectKind::Segment { segment } = &segment_object.kind else {
2190                return Err(make_err(format!(
2191                    "Object is not a segment, it is {}",
2192                    segment_object.kind.human_friendly_kind_with_article()
2193                )));
2194            };
2195            let Segment::Arc(arc) = segment else {
2196                return Err(make_err(format!(
2197                    "Segment is not an arc, it is {}",
2198                    segment.human_friendly_kind_with_article()
2199                )));
2200            };
2201            vec![arc.start, arc.end, arc.center, segment_id]
2202        };
2203        let src_delta = SourceDelta { text: new_source };
2204        // Uses .no_freedom_analysis() so freedom_analysis: false
2205        let outcome = self.update_state_after_exec(outcome, false);
2206        let scene_graph_delta = SceneGraphDelta {
2207            new_graph: self.scene_graph_for_ui(),
2208            invalidates_ids: false,
2209            new_objects: new_object_ids,
2210            exec_outcome: outcome,
2211        };
2212        Ok((src_delta, scene_graph_delta))
2213    }
2214
2215    async fn add_circle(
2216        &mut self,
2217        ctx: &ExecutorContext,
2218        sketch: ObjectId,
2219        ctor: CircleCtor,
2220    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2221        // Create updated KCL source from args.
2222        let start_ast = to_ast_point2d(&ctor.start)
2223            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2224        let center_ast = to_ast_point2d(&ctor.center)
2225            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2226        let mut arguments = vec![
2227            ast::LabeledArg {
2228                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2229                arg: start_ast,
2230            },
2231            ast::LabeledArg {
2232                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2233                arg: center_ast,
2234            },
2235        ];
2236        // Add construction kwarg if construction is Some(true)
2237        if ctor.construction == Some(true) {
2238            arguments.push(ast::LabeledArg {
2239                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2240                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2241                    value: ast::LiteralValue::Bool(true),
2242                    raw: "true".to_string(),
2243                    digest: None,
2244                }))),
2245            });
2246        }
2247        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2248            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2249            unlabeled: None,
2250            arguments,
2251            digest: None,
2252            non_code_meta: Default::default(),
2253        })));
2254
2255        // Look up existing sketch.
2256        let sketch_id = sketch;
2257        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2258            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2259        })?;
2260        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2261            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2262                "Object is not a sketch, it is {}",
2263                sketch_object.kind.human_friendly_kind_with_article(),
2264            ))));
2265        };
2266        // Add the circle to the AST of the sketch block.
2267        let mut new_ast = self.program.ast.clone();
2268        let (sketch_block_ref, _) = self
2269            .mutate_ast(
2270                &mut new_ast,
2271                sketch_id,
2272                AstMutateCommand::AddSketchBlockVarDecl {
2273                    prefix: CIRCLE_VARIABLE.to_owned(),
2274                    expr: circle_ast,
2275                },
2276            )
2277            .map_err(KclErrorWithOutputs::no_outputs)?;
2278        // Convert to string source to create real source ranges.
2279        let new_source = source_from_ast(&new_ast);
2280        // Parse the new KCL source.
2281        let (new_program, errors) = Program::parse(&new_source)
2282            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2283        if !errors.is_empty() {
2284            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2285                "Error parsing KCL source after adding circle: {errors:?}"
2286            ))));
2287        }
2288        let Some(new_program) = new_program else {
2289            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2290                "No AST produced after adding circle".to_string(),
2291            )));
2292        };
2293
2294        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2295            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2296                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2297            )))
2298        })?;
2299
2300        // Make sure to only set this if there are no errors.
2301        self.program = new_program.clone();
2302
2303        // Truncate after the sketch block for mock execution.
2304        let mut truncated_program = new_program;
2305        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2306            .map_err(KclErrorWithOutputs::no_outputs)?;
2307
2308        // Execute.
2309        let outcome = ctx
2310            .run_mock(
2311                &truncated_program,
2312                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2313            )
2314            .await?;
2315
2316        let new_object_ids = {
2317            let make_err =
2318                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2319            let segment_id = outcome
2320                .source_range_to_object
2321                .get(&circle_node_ref.range)
2322                .copied()
2323                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2324            let segment_object = outcome
2325                .scene_objects
2326                .get(segment_id.0)
2327                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2328            let ObjectKind::Segment { segment } = &segment_object.kind else {
2329                return Err(make_err(format!(
2330                    "Object is not a segment, it is {}",
2331                    segment_object.kind.human_friendly_kind_with_article()
2332                )));
2333            };
2334            let Segment::Circle(circle) = segment else {
2335                return Err(make_err(format!(
2336                    "Segment is not a circle, it is {}",
2337                    segment.human_friendly_kind_with_article()
2338                )));
2339            };
2340            vec![circle.start, circle.center, segment_id]
2341        };
2342        let src_delta = SourceDelta { text: new_source };
2343        // Uses .no_freedom_analysis() so freedom_analysis: false
2344        let outcome = self.update_state_after_exec(outcome, false);
2345        let scene_graph_delta = SceneGraphDelta {
2346            new_graph: self.scene_graph_for_ui(),
2347            invalidates_ids: false,
2348            new_objects: new_object_ids,
2349            exec_outcome: outcome,
2350        };
2351        Ok((src_delta, scene_graph_delta))
2352    }
2353
2354    async fn add_control_point_spline(
2355        &mut self,
2356        ctx: &ExecutorContext,
2357        sketch: ObjectId,
2358        ctor: ControlPointSplineCtor,
2359    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2360        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2361            .map_err(KclErrorWithOutputs::no_outputs)?;
2362
2363        let points_ast = to_ast_point2d_array(&ctor.points)
2364            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2365        let mut arguments = vec![ast::LabeledArg {
2366            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2367            arg: points_ast,
2368        }];
2369        if ctor.construction == Some(true) {
2370            arguments.push(ast::LabeledArg {
2371                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2372                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2373                    value: ast::LiteralValue::Bool(true),
2374                    raw: "true".to_string(),
2375                    digest: None,
2376                }))),
2377            });
2378        }
2379        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2380            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2381            unlabeled: None,
2382            arguments,
2383            digest: None,
2384            non_code_meta: Default::default(),
2385        })));
2386
2387        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2388            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2389        })?;
2390        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2391            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2392                "Object is not a sketch, it is {}",
2393                sketch_object.kind.human_friendly_kind_with_article(),
2394            ))));
2395        };
2396
2397        let mut new_ast = new_program.ast.clone();
2398        let (sketch_block_ref, _) = self
2399            .mutate_ast(
2400                &mut new_ast,
2401                sketch,
2402                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2403            )
2404            .map_err(KclErrorWithOutputs::no_outputs)?;
2405        let new_source = source_from_ast(&new_ast);
2406        let (new_program, errors) = Program::parse(&new_source)
2407            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2408        if !errors.is_empty() {
2409            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410                "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2411            ))));
2412        }
2413        let Some(new_program) = new_program else {
2414            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2415                "No AST produced after adding controlPointSpline".to_string(),
2416            )));
2417        };
2418
2419        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2420            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2421                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2422            )))
2423        })?;
2424        #[cfg(not(feature = "artifact-graph"))]
2425        let _ = spline_node_ref;
2426
2427        self.program = new_program.clone();
2428
2429        let mut truncated_program = new_program;
2430        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2431            .map_err(KclErrorWithOutputs::no_outputs)?;
2432
2433        let outcome = ctx
2434            .run_mock(
2435                &truncated_program,
2436                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2437            )
2438            .await?;
2439
2440        #[cfg(not(feature = "artifact-graph"))]
2441        let new_object_ids = Vec::new();
2442        #[cfg(feature = "artifact-graph")]
2443        let new_object_ids = {
2444            let make_err =
2445                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2446            let segment_id = outcome
2447                .source_range_to_object
2448                .get(&spline_node_ref.range)
2449                .copied()
2450                .ok_or_else(|| {
2451                    make_err(format!(
2452                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2453                    ))
2454                })?;
2455            let segment_object = outcome
2456                .scene_objects
2457                .get(segment_id.0)
2458                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2459            let ObjectKind::Segment { segment } = &segment_object.kind else {
2460                return Err(make_err(format!(
2461                    "Object is not a segment, it is {}",
2462                    segment_object.kind.human_friendly_kind_with_article()
2463                )));
2464            };
2465            let Segment::ControlPointSpline(spline) = segment else {
2466                return Err(make_err(format!(
2467                    "Segment is not a control point spline, it is {}",
2468                    segment.human_friendly_kind_with_article()
2469                )));
2470            };
2471
2472            let mut ids = outcome
2473                .scene_objects
2474                .iter()
2475                .filter_map(|obj| match &obj.kind {
2476                    ObjectKind::Segment {
2477                        segment: Segment::Line(line),
2478                    } if line.owner == Some(segment_id) => Some(obj.id),
2479                    _ => None,
2480                })
2481                .collect::<Vec<_>>();
2482            ids.extend(spline.controls.clone());
2483            ids.push(segment_id);
2484            ids
2485        };
2486        let src_delta = SourceDelta { text: new_source };
2487        let outcome = self.update_state_after_exec(outcome, false);
2488        let scene_graph_delta = SceneGraphDelta {
2489            new_graph: self.scene_graph_for_ui(),
2490            invalidates_ids: false,
2491            new_objects: new_object_ids,
2492            exec_outcome: outcome,
2493        };
2494        Ok((src_delta, scene_graph_delta))
2495    }
2496
2497    fn edit_point(
2498        &mut self,
2499        new_ast: &mut ast::Node<ast::Program>,
2500        sketch: ObjectId,
2501        point: ObjectId,
2502        ctor: PointCtor,
2503    ) -> Result<(), KclError> {
2504        // Create updated KCL source from args.
2505        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2506
2507        // Look up existing sketch.
2508        let sketch_id = sketch;
2509        let sketch_object = self
2510            .scene_graph
2511            .objects
2512            .get(sketch_id.0)
2513            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2514        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2515            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2516        };
2517        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2518            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2519        })?;
2520        // Look up existing point.
2521        let point_id = point;
2522        let point_object = self
2523            .scene_graph
2524            .objects
2525            .get(point_id.0)
2526            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2527        let ObjectKind::Segment {
2528            segment: Segment::Point(point),
2529        } = &point_object.kind
2530        else {
2531            return Err(KclError::refactor(format!(
2532                "Object is not a point segment: {point_object:?}"
2533            )));
2534        };
2535
2536        // If the point is part of a line or arc, edit the line/arc instead.
2537        if let Some(owner_id) = point.owner {
2538            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2539                KclError::refactor(format!(
2540                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2541                ))
2542            })?;
2543            let ObjectKind::Segment { segment } = &owner_object.kind else {
2544                return Err(KclError::refactor(format!(
2545                    "Internal: Owner of point is not a segment, but found {}",
2546                    owner_object.kind.human_friendly_kind_with_article()
2547                )));
2548            };
2549
2550            // Handle Line owner
2551            if let Segment::Line(line) = segment {
2552                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2553                    return Err(KclError::refactor(format!(
2554                        "Internal: Owner of point does not have line ctor, but found {}",
2555                        line.ctor.human_friendly_kind_with_article()
2556                    )));
2557                };
2558                let mut line_ctor = line_ctor.clone();
2559                // Which end of the line is this point?
2560                if line.start == point_id {
2561                    line_ctor.start = ctor.position;
2562                } else if line.end == point_id {
2563                    line_ctor.end = ctor.position;
2564                } else {
2565                    return Err(KclError::refactor(format!(
2566                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2567                    )));
2568                }
2569                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2570            }
2571
2572            // Handle Arc owner
2573            if let Segment::Arc(arc) = segment {
2574                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2575                    return Err(KclError::refactor(format!(
2576                        "Internal: Owner of point does not have arc ctor, but found {}",
2577                        arc.ctor.human_friendly_kind_with_article()
2578                    )));
2579                };
2580                let mut arc_ctor = arc_ctor.clone();
2581                // Which point of the arc is this? (center, start, or end)
2582                if arc.center == point_id {
2583                    arc_ctor.center = ctor.position;
2584                } else if arc.start == point_id {
2585                    arc_ctor.start = ctor.position;
2586                } else if arc.end == point_id {
2587                    arc_ctor.end = ctor.position;
2588                } else {
2589                    return Err(KclError::refactor(format!(
2590                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2591                    )));
2592                }
2593                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2594            }
2595
2596            // Handle Circle owner
2597            if let Segment::Circle(circle) = segment {
2598                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2599                    return Err(KclError::refactor(format!(
2600                        "Internal: Owner of point does not have circle ctor, but found {}",
2601                        circle.ctor.human_friendly_kind_with_article()
2602                    )));
2603                };
2604                let mut circle_ctor = circle_ctor.clone();
2605                if circle.center == point_id {
2606                    circle_ctor.center = ctor.position;
2607                } else if circle.start == point_id {
2608                    circle_ctor.start = ctor.position;
2609                } else {
2610                    return Err(KclError::refactor(format!(
2611                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2612                    )));
2613                }
2614                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2615            }
2616
2617            if let Segment::ControlPointSpline(spline) = segment {
2618                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2619                    return Err(KclError::refactor(format!(
2620                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2621                        spline.ctor.human_friendly_kind_with_article()
2622                    )));
2623                };
2624                let mut spline_ctor = spline_ctor.clone();
2625                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2626                    return Err(KclError::refactor(format!(
2627                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2628                    )));
2629                };
2630                spline_ctor.points[control_index] = ctor.position;
2631                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2632            }
2633
2634            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2635            // (fall through to the point editing logic below)
2636        }
2637
2638        // Modify the point AST.
2639        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2640        Ok(())
2641    }
2642
2643    fn edit_line(
2644        &mut self,
2645        new_ast: &mut ast::Node<ast::Program>,
2646        sketch: ObjectId,
2647        line: ObjectId,
2648        ctor: LineCtor,
2649    ) -> Result<(), KclError> {
2650        // Create updated KCL source from args.
2651        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2652        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2653
2654        // Look up existing sketch.
2655        let sketch_id = sketch;
2656        let sketch_object = self
2657            .scene_graph
2658            .objects
2659            .get(sketch_id.0)
2660            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2661        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2662            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2663        };
2664        sketch
2665            .segments
2666            .iter()
2667            .find(|o| **o == line)
2668            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2669        // Look up existing line.
2670        let line_id = line;
2671        let line_object = self
2672            .scene_graph
2673            .objects
2674            .get(line_id.0)
2675            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2676        let ObjectKind::Segment { .. } = &line_object.kind else {
2677            let kind = line_object.kind.human_friendly_kind_with_article();
2678            return Err(KclError::refactor(format!(
2679                "This constraint only works on Segments, but you selected {kind}"
2680            )));
2681        };
2682
2683        // Modify the line AST.
2684        self.mutate_ast(
2685            new_ast,
2686            line_id,
2687            AstMutateCommand::EditLine {
2688                start: new_start_ast,
2689                end: new_end_ast,
2690                construction: ctor.construction,
2691            },
2692        )?;
2693        Ok(())
2694    }
2695
2696    fn edit_arc(
2697        &mut self,
2698        new_ast: &mut ast::Node<ast::Program>,
2699        sketch: ObjectId,
2700        arc: ObjectId,
2701        ctor: ArcCtor,
2702    ) -> Result<(), KclError> {
2703        // Create updated KCL source from args.
2704        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2705        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2706        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2707
2708        // Look up existing sketch.
2709        let sketch_id = sketch;
2710        let sketch_object = self
2711            .scene_graph
2712            .objects
2713            .get(sketch_id.0)
2714            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2715        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2716            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2717        };
2718        sketch
2719            .segments
2720            .iter()
2721            .find(|o| **o == arc)
2722            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2723        // Look up existing arc.
2724        let arc_id = arc;
2725        let arc_object = self
2726            .scene_graph
2727            .objects
2728            .get(arc_id.0)
2729            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2730        let ObjectKind::Segment { .. } = &arc_object.kind else {
2731            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2732        };
2733
2734        // Modify the arc AST.
2735        self.mutate_ast(
2736            new_ast,
2737            arc_id,
2738            AstMutateCommand::EditArc {
2739                start: new_start_ast,
2740                end: new_end_ast,
2741                center: new_center_ast,
2742                construction: ctor.construction,
2743            },
2744        )?;
2745        Ok(())
2746    }
2747
2748    fn edit_circle(
2749        &mut self,
2750        new_ast: &mut ast::Node<ast::Program>,
2751        sketch: ObjectId,
2752        circle: ObjectId,
2753        ctor: CircleCtor,
2754    ) -> Result<(), KclError> {
2755        // Create updated KCL source from args.
2756        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2757        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2758
2759        // Look up existing sketch.
2760        let sketch_id = sketch;
2761        let sketch_object = self
2762            .scene_graph
2763            .objects
2764            .get(sketch_id.0)
2765            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2766        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2767            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2768        };
2769        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2770            KclError::refactor(format!(
2771                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2772            ))
2773        })?;
2774        // Look up existing circle.
2775        let circle_id = circle;
2776        let circle_object = self
2777            .scene_graph
2778            .objects
2779            .get(circle_id.0)
2780            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2781        let ObjectKind::Segment { .. } = &circle_object.kind else {
2782            return Err(KclError::refactor(format!(
2783                "Object is not a segment: {circle_object:?}"
2784            )));
2785        };
2786
2787        // Modify the circle AST.
2788        self.mutate_ast(
2789            new_ast,
2790            circle_id,
2791            AstMutateCommand::EditCircle {
2792                start: new_start_ast,
2793                center: new_center_ast,
2794                construction: ctor.construction,
2795            },
2796        )?;
2797        Ok(())
2798    }
2799
2800    fn edit_control_point_spline(
2801        &mut self,
2802        new_ast: &mut ast::Node<ast::Program>,
2803        sketch: ObjectId,
2804        spline: ObjectId,
2805        ctor: ControlPointSplineCtor,
2806    ) -> Result<(), KclError> {
2807        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2808
2809        let sketch_object = self
2810            .scene_graph
2811            .objects
2812            .get(sketch.0)
2813            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2814        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2815            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2816        };
2817        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2818            KclError::refactor(format!(
2819                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2820            ))
2821        })?;
2822
2823        let spline_object =
2824            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2825                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2826            })?;
2827        let ObjectKind::Segment { .. } = &spline_object.kind else {
2828            return Err(KclError::refactor(format!(
2829                "Object is not a segment: {spline_object:?}"
2830            )));
2831        };
2832
2833        self.mutate_ast(
2834            new_ast,
2835            spline,
2836            AstMutateCommand::EditControlPointSpline {
2837                points: points_ast,
2838                construction: ctor.construction,
2839            },
2840        )?;
2841        Ok(())
2842    }
2843
2844    fn delete_segment(
2845        &mut self,
2846        new_ast: &mut ast::Node<ast::Program>,
2847        sketch: ObjectId,
2848        segment_id: ObjectId,
2849    ) -> Result<(), KclError> {
2850        // Look up existing sketch.
2851        let sketch_id = sketch;
2852        let sketch_object = self
2853            .scene_graph
2854            .objects
2855            .get(sketch_id.0)
2856            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2857        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2858            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2859        };
2860        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2861            KclError::refactor(format!(
2862                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2863            ))
2864        })?;
2865        // Look up existing segment.
2866        let segment_object =
2867            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2868                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2869            })?;
2870        let ObjectKind::Segment { .. } = &segment_object.kind else {
2871            return Err(KclError::refactor(format!(
2872                "Object is not a segment, it is {}",
2873                segment_object.kind.human_friendly_kind_with_article()
2874            )));
2875        };
2876
2877        // Modify the AST to remove the segment.
2878        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2879        Ok(())
2880    }
2881
2882    fn delete_constraint(
2883        &mut self,
2884        new_ast: &mut ast::Node<ast::Program>,
2885        sketch: ObjectId,
2886        constraint_id: ObjectId,
2887    ) -> Result<(), KclError> {
2888        // Look up existing sketch.
2889        let sketch_id = sketch;
2890        let sketch_object = self
2891            .scene_graph
2892            .objects
2893            .get(sketch_id.0)
2894            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897        };
2898        sketch
2899            .constraints
2900            .iter()
2901            .find(|o| **o == constraint_id)
2902            .ok_or_else(|| {
2903                KclError::refactor(format!(
2904                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2905                ))
2906            })?;
2907        // Look up existing constraint.
2908        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2909            KclError::refactor(format!(
2910                "Constraint not found in scene graph: constraint={constraint_id:?}"
2911            ))
2912        })?;
2913        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2914            return Err(KclError::refactor(format!(
2915                "Object is not a constraint, it is {}",
2916                constraint_object.kind.human_friendly_kind_with_article()
2917            )));
2918        };
2919
2920        // Modify the AST to remove the constraint.
2921        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2922        Ok(())
2923    }
2924
2925    fn edit_coincident_constraint(
2926        &mut self,
2927        new_ast: &mut ast::Node<ast::Program>,
2928        constraint_id: ObjectId,
2929        segments: Vec<ConstraintSegment>,
2930    ) -> Result<(), KclError> {
2931        if segments.len() < 2 {
2932            return Err(KclError::refactor(format!(
2933                "Coincident constraint must have at least 2 inputs, got {}",
2934                segments.len()
2935            )));
2936        }
2937
2938        let segment_asts = segments
2939            .iter()
2940            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2941            .collect::<Result<Vec<_>, _>>()?;
2942
2943        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2944            elements: segment_asts,
2945            digest: None,
2946            non_code_meta: Default::default(),
2947        })));
2948
2949        self.mutate_ast(
2950            new_ast,
2951            constraint_id,
2952            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2953        )?;
2954        Ok(())
2955    }
2956
2957    fn edit_horizontal_points_constraint(
2958        &mut self,
2959        new_ast: &mut ast::Node<ast::Program>,
2960        constraint_id: ObjectId,
2961        points: Vec<ConstraintSegment>,
2962    ) -> Result<(), KclError> {
2963        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2964    }
2965
2966    fn edit_vertical_points_constraint(
2967        &mut self,
2968        new_ast: &mut ast::Node<ast::Program>,
2969        constraint_id: ObjectId,
2970        points: Vec<ConstraintSegment>,
2971    ) -> Result<(), KclError> {
2972        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2973    }
2974
2975    fn edit_axis_points_constraint(
2976        &mut self,
2977        new_ast: &mut ast::Node<ast::Program>,
2978        constraint_id: ObjectId,
2979        points: Vec<ConstraintSegment>,
2980        constraint_name: &str,
2981    ) -> Result<(), KclError> {
2982        if points.len() < 2 {
2983            return Err(KclError::refactor(format!(
2984                "{constraint_name} points constraint must have at least 2 points, got {}",
2985                points.len()
2986            )));
2987        }
2988
2989        let point_asts = points
2990            .iter()
2991            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2992            .collect::<Result<Vec<_>, _>>()?;
2993
2994        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2995            elements: point_asts,
2996            digest: None,
2997            non_code_meta: Default::default(),
2998        })));
2999
3000        self.mutate_ast(
3001            new_ast,
3002            constraint_id,
3003            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3004        )?;
3005        Ok(())
3006    }
3007
3008    /// updates the equalLength constraint with the given lines
3009    fn edit_equal_length_constraint(
3010        &mut self,
3011        new_ast: &mut ast::Node<ast::Program>,
3012        constraint_id: ObjectId,
3013        lines: Vec<ObjectId>,
3014    ) -> Result<(), KclError> {
3015        if lines.len() < 2 {
3016            return Err(KclError::refactor(format!(
3017                "Lines equal length constraint must have at least 2 lines, got {}",
3018                lines.len()
3019            )));
3020        }
3021
3022        let line_asts = lines
3023            .iter()
3024            .map(|line_id| {
3025                let line_object = self
3026                    .scene_graph
3027                    .objects
3028                    .get(line_id.0)
3029                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3030                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3031                    let kind = line_object.kind.human_friendly_kind_with_article();
3032                    return Err(KclError::refactor(format!(
3033                        "This constraint only works on Segments, but you selected {kind}"
3034                    )));
3035                };
3036                let Segment::Line(_) = line_segment else {
3037                    let kind = line_segment.human_friendly_kind_with_article();
3038                    return Err(KclError::refactor(format!(
3039                        "Only lines can be made equal length, but you selected {kind}"
3040                    )));
3041                };
3042
3043                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3044            })
3045            .collect::<Result<Vec<_>, _>>()?;
3046
3047        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3048            elements: line_asts,
3049            digest: None,
3050            non_code_meta: Default::default(),
3051        })));
3052
3053        self.mutate_ast(
3054            new_ast,
3055            constraint_id,
3056            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3057        )?;
3058        Ok(())
3059    }
3060
3061    /// Updates the parallel constraint with the given lines.
3062    fn edit_parallel_constraint(
3063        &mut self,
3064        new_ast: &mut ast::Node<ast::Program>,
3065        constraint_id: ObjectId,
3066        lines: Vec<ObjectId>,
3067    ) -> Result<(), KclError> {
3068        if lines.len() < 2 {
3069            return Err(KclError::refactor(format!(
3070                "Parallel constraint must have at least 2 lines, got {}",
3071                lines.len()
3072            )));
3073        }
3074
3075        let line_asts = lines
3076            .iter()
3077            .map(|line_id| {
3078                let line_object = self
3079                    .scene_graph
3080                    .objects
3081                    .get(line_id.0)
3082                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3083                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3084                    let kind = line_object.kind.human_friendly_kind_with_article();
3085                    return Err(KclError::refactor(format!(
3086                        "This constraint only works on Segments, but you selected {kind}"
3087                    )));
3088                };
3089                let Segment::Line(_) = line_segment else {
3090                    let kind = line_segment.human_friendly_kind_with_article();
3091                    return Err(KclError::refactor(format!(
3092                        "Only lines can be made parallel, but you selected {kind}"
3093                    )));
3094                };
3095
3096                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3097            })
3098            .collect::<Result<Vec<_>, _>>()?;
3099
3100        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3101            elements: line_asts,
3102            digest: None,
3103            non_code_meta: Default::default(),
3104        })));
3105
3106        self.mutate_ast(
3107            new_ast,
3108            constraint_id,
3109            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3110        )?;
3111        Ok(())
3112    }
3113
3114    /// Updates the equalRadius constraint with the given segments.
3115    fn edit_equal_radius_constraint(
3116        &mut self,
3117        new_ast: &mut ast::Node<ast::Program>,
3118        constraint_id: ObjectId,
3119        input: Vec<ObjectId>,
3120    ) -> Result<(), KclError> {
3121        if input.len() < 2 {
3122            return Err(KclError::refactor(format!(
3123                "equalRadius constraint must have at least 2 segments, got {}",
3124                input.len()
3125            )));
3126        }
3127
3128        let input_asts = input
3129            .iter()
3130            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3131            .collect::<Result<Vec<_>, _>>()?;
3132
3133        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3134            elements: input_asts,
3135            digest: None,
3136            non_code_meta: Default::default(),
3137        })));
3138
3139        self.mutate_ast(
3140            new_ast,
3141            constraint_id,
3142            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3143        )?;
3144        Ok(())
3145    }
3146
3147    async fn execute_after_edit(
3148        &mut self,
3149        ctx: &ExecutorContext,
3150        sketch: ObjectId,
3151        sketch_block_ref: AstNodeRef,
3152        segment_ids_edited: AhashIndexSet<ObjectId>,
3153        edit_kind: EditDeleteKind,
3154        new_ast: &mut ast::Node<ast::Program>,
3155    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3156        // Convert to string source to create real source ranges.
3157        let new_source = source_from_ast(new_ast);
3158        // Parse the new KCL source.
3159        let (new_program, errors) = Program::parse(&new_source)
3160            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3161        if !errors.is_empty() {
3162            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3163                "Error parsing KCL source after editing: {errors:?}"
3164            ))));
3165        }
3166        let Some(new_program) = new_program else {
3167            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3168                "No AST produced after editing".to_string(),
3169            )));
3170        };
3171
3172        // TODO: sketch-api: make sure to only set this if there are no errors.
3173        self.program = new_program.clone();
3174
3175        // Truncate after the sketch block for mock execution.
3176        let is_delete = edit_kind.is_delete();
3177        let truncated_program = {
3178            let mut truncated_program = new_program;
3179            only_sketch_block(
3180                &mut truncated_program.ast,
3181                &sketch_block_ref,
3182                edit_kind.to_change_kind(),
3183            )
3184            .map_err(KclErrorWithOutputs::no_outputs)?;
3185            truncated_program
3186        };
3187
3188        // Execute.
3189        let mock_config = MockConfig {
3190            sketch_block_id: Some(sketch),
3191            freedom_analysis: is_delete,
3192            segment_ids_edited: segment_ids_edited.clone(),
3193            ..Default::default()
3194        };
3195        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3196
3197        // Uses freedom_analysis: is_delete
3198        let outcome = self.update_state_after_exec(outcome, is_delete);
3199
3200        let new_source = {
3201            // Feed back sketch var solutions into the source.
3202            //
3203            // The interpreter is returning all var solutions from the sketch
3204            // block we're editing.
3205            let mut new_ast = self.program.ast.clone();
3206            for (var_range, value) in &outcome.var_solutions {
3207                let rounded = value.round(3);
3208                let source_ref = SourceRef::Simple {
3209                    range: *var_range,
3210                    node_path: None,
3211                };
3212                mutate_ast_node_by_source_ref(
3213                    &mut new_ast,
3214                    &source_ref,
3215                    AstMutateCommand::EditVarInitialValue { value: rounded },
3216                )
3217                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
3218            }
3219            source_from_ast(&new_ast)
3220        };
3221
3222        let src_delta = SourceDelta { text: new_source };
3223        let scene_graph_delta = SceneGraphDelta {
3224            new_graph: self.scene_graph_for_ui(),
3225            invalidates_ids: is_delete,
3226            new_objects: Vec::new(),
3227            exec_outcome: outcome,
3228        };
3229        Ok((src_delta, scene_graph_delta))
3230    }
3231
3232    async fn execute_after_delete_sketch(
3233        &mut self,
3234        ctx: &ExecutorContext,
3235        new_ast: &mut ast::Node<ast::Program>,
3236    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3237        // Convert to string source to create real source ranges.
3238        let new_source = source_from_ast(new_ast);
3239        // Parse the new KCL source.
3240        let (new_program, errors) = Program::parse(&new_source)
3241            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3242        if !errors.is_empty() {
3243            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3244                "Error parsing KCL source after editing: {errors:?}"
3245            ))));
3246        }
3247        let Some(new_program) = new_program else {
3248            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3249                "No AST produced after editing".to_string(),
3250            )));
3251        };
3252
3253        // Make sure to only set this if there are no errors.
3254        self.program = new_program.clone();
3255
3256        // We deleted the entire sketch block. It doesn't make sense to truncate
3257        // and execute only the sketch block. We execute the whole program with
3258        // a real engine.
3259
3260        // Execute.
3261        let outcome = ctx.run_with_caching(new_program).await?;
3262        let freedom_analysis_ran = true;
3263
3264        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3265
3266        let src_delta = SourceDelta { text: new_source };
3267        let scene_graph_delta = SceneGraphDelta {
3268            new_graph: self.scene_graph_for_ui(),
3269            invalidates_ids: true,
3270            new_objects: Vec::new(),
3271            exec_outcome: outcome,
3272        };
3273        Ok((src_delta, scene_graph_delta))
3274    }
3275
3276    /// Map a point object id into an AST reference expression for use in
3277    /// constraints. If the point is owned by a segment (line or arc), we
3278    /// reference the appropriate property on that segment (e.g. `line1.start`,
3279    /// `arc1.center`). Otherwise we reference the point directly.
3280    fn point_id_to_ast_reference(
3281        &self,
3282        point_id: ObjectId,
3283        new_ast: &mut ast::Node<ast::Program>,
3284    ) -> Result<ast::Expr, KclError> {
3285        let point_object = self
3286            .scene_graph
3287            .objects
3288            .get(point_id.0)
3289            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3290        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3291            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3292        };
3293        let Segment::Point(point) = point_segment else {
3294            return Err(KclError::refactor(format!(
3295                "Only points are currently supported: {point_object:?}"
3296            )));
3297        };
3298
3299        if let Some(owner_id) = point.owner {
3300            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3301                KclError::refactor(format!(
3302                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3303                ))
3304            })?;
3305            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3306                return Err(KclError::refactor(format!(
3307                    "Owner of point is not a segment, but found {}",
3308                    owner_object.kind.human_friendly_kind_with_article()
3309                )));
3310            };
3311
3312            match owner_segment {
3313                Segment::Line(line) => {
3314                    let property = if line.start == point_id {
3315                        LINE_PROPERTY_START
3316                    } else if line.end == point_id {
3317                        LINE_PROPERTY_END
3318                    } else {
3319                        return Err(KclError::refactor(format!(
3320                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3321                        )));
3322                    };
3323                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3324                }
3325                Segment::Arc(arc) => {
3326                    let property = if arc.start == point_id {
3327                        ARC_PROPERTY_START
3328                    } else if arc.end == point_id {
3329                        ARC_PROPERTY_END
3330                    } else if arc.center == point_id {
3331                        ARC_PROPERTY_CENTER
3332                    } else {
3333                        return Err(KclError::refactor(format!(
3334                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3335                        )));
3336                    };
3337                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3338                }
3339                Segment::Circle(circle) => {
3340                    let property = if circle.start == point_id {
3341                        CIRCLE_PROPERTY_START
3342                    } else if circle.center == point_id {
3343                        CIRCLE_PROPERTY_CENTER
3344                    } else {
3345                        return Err(KclError::refactor(format!(
3346                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3347                        )));
3348                    };
3349                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3350                }
3351                Segment::ControlPointSpline(spline) => {
3352                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3353                        return Err(KclError::refactor(format!(
3354                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3355                        )));
3356                    };
3357                    let owner_expr =
3358                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3359                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3360                    Ok(create_index_expression(controls_expr, index))
3361                }
3362                _ => Err(KclError::refactor(format!(
3363                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3364                ))),
3365            }
3366        } else {
3367            // Standalone point.
3368            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3369        }
3370    }
3371
3372    fn line_id_to_ast_reference(
3373        &self,
3374        line_id: ObjectId,
3375        new_ast: &mut ast::Node<ast::Program>,
3376    ) -> Result<ast::Expr, KclError> {
3377        let line_object = self
3378            .scene_graph
3379            .objects
3380            .get(line_id.0)
3381            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3382        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3383            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3384        };
3385        let Segment::Line(line) = line_segment else {
3386            return Err(KclError::refactor(format!(
3387                "Only lines are currently supported: {line_object:?}"
3388            )));
3389        };
3390
3391        if let Some(owner_id) = line.owner {
3392            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3393                KclError::refactor(format!(
3394                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3395                ))
3396            })?;
3397            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3398                return Err(KclError::refactor(format!(
3399                    "Owner of line is not a segment, but found {}",
3400                    owner_object.kind.human_friendly_kind_with_article()
3401                )));
3402            };
3403
3404            match owner_segment {
3405                Segment::ControlPointSpline(spline) => {
3406                    let Some(index) = spline
3407                        .controls
3408                        .windows(2)
3409                        .position(|window| window[0] == line.start && window[1] == line.end)
3410                    else {
3411                        return Err(KclError::refactor(format!(
3412                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3413                        )));
3414                    };
3415                    let owner_expr =
3416                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3417                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3418                    Ok(create_index_expression(edges_expr, index))
3419                }
3420                _ => Err(KclError::refactor(format!(
3421                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3422                ))),
3423            }
3424        } else {
3425            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3426        }
3427    }
3428
3429    fn coincident_segment_to_ast(
3430        &self,
3431        segment: &ConstraintSegment,
3432        new_ast: &mut ast::Node<ast::Program>,
3433    ) -> Result<ast::Expr, KclError> {
3434        match segment {
3435            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3436            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3437        }
3438    }
3439
3440    fn segment_id_to_constraint_ast_reference(
3441        &self,
3442        segment_id: ObjectId,
3443        new_ast: &mut ast::Node<ast::Program>,
3444    ) -> Result<ast::Expr, KclError> {
3445        let segment_object = self
3446            .scene_graph
3447            .objects
3448            .get(segment_id.0)
3449            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3450        let ObjectKind::Segment { segment } = &segment_object.kind else {
3451            return Err(KclError::refactor(format!(
3452                "Object is not a segment, it is {}",
3453                segment_object.kind.human_friendly_kind_with_article()
3454            )));
3455        };
3456
3457        match segment {
3458            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3459            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3460            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3461            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3462            Segment::ControlPointSpline(_) => {
3463                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3464            }
3465        }
3466    }
3467
3468    fn axis_constraint_segment_to_ast(
3469        &self,
3470        segment: &ConstraintSegment,
3471        new_ast: &mut ast::Node<ast::Program>,
3472    ) -> Result<ast::Expr, KclError> {
3473        match segment {
3474            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3475            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3476        }
3477    }
3478
3479    async fn add_coincident(
3480        &mut self,
3481        sketch: ObjectId,
3482        coincident: Coincident,
3483        new_ast: &mut ast::Node<ast::Program>,
3484    ) -> Result<AstNodeRef, KclError> {
3485        let sketch_id = sketch;
3486        for segment in &coincident.segments {
3487            let ConstraintSegment::Segment(segment_id) = segment else {
3488                continue;
3489            };
3490            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3491                continue;
3492            };
3493            if matches!(
3494                segment_object.kind,
3495                ObjectKind::Segment {
3496                    segment: Segment::ControlPointSpline(_)
3497                }
3498            ) {
3499                return Err(KclError::refactor(
3500                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3501                        .to_owned(),
3502                ));
3503            }
3504        }
3505        let segment_asts = coincident
3506            .segments
3507            .iter()
3508            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3509            .collect::<Result<Vec<_>, _>>()?;
3510        if segment_asts.len() < 2 {
3511            return Err(KclError::refactor(format!(
3512                "Coincident constraint must have at least 2 inputs, got {}",
3513                segment_asts.len()
3514            )));
3515        }
3516
3517        // Create the coincident() call using shared helper.
3518        let coincident_ast = create_coincident_ast(segment_asts);
3519
3520        // Add the line to the AST of the sketch block.
3521        let (sketch_block_ref, _) = self.mutate_ast(
3522            new_ast,
3523            sketch_id,
3524            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3525        )?;
3526        Ok(sketch_block_ref)
3527    }
3528
3529    async fn add_distance(
3530        &mut self,
3531        sketch: ObjectId,
3532        distance: Distance,
3533        new_ast: &mut ast::Node<ast::Program>,
3534    ) -> Result<AstNodeRef, KclError> {
3535        let sketch_id = sketch;
3536        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3537            [pt0, pt1] => [
3538                self.coincident_segment_to_ast(pt0, new_ast)?,
3539                self.coincident_segment_to_ast(pt1, new_ast)?,
3540            ],
3541            _ => {
3542                return Err(KclError::refactor(format!(
3543                    "Distance constraint must have exactly 2 points, got {}",
3544                    distance.points.len()
3545                )));
3546            }
3547        };
3548
3549        let arguments = match &distance.label_position {
3550            Some(label_position) => vec![ast::LabeledArg {
3551                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3552                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3553            }],
3554            None => Default::default(),
3555        };
3556
3557        // Create the distance() call.
3558        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3559            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3560            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3561                ast::ArrayExpression {
3562                    elements: vec![pt0_ast, pt1_ast],
3563                    digest: None,
3564                    non_code_meta: Default::default(),
3565                },
3566            )))),
3567            arguments,
3568            digest: None,
3569            non_code_meta: Default::default(),
3570        })));
3571        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3572            left: distance_call_ast,
3573            operator: ast::BinaryOperator::Eq,
3574            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3575                value: ast::LiteralValue::Number {
3576                    value: distance.distance.value,
3577                    suffix: distance.distance.units,
3578                },
3579                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3580                    KclError::refactor(format!(
3581                        "Could not format numeric suffix: {:?}",
3582                        distance.distance.units
3583                    ))
3584                })?,
3585                digest: None,
3586            }))),
3587            digest: None,
3588        })));
3589
3590        // Add the line to the AST of the sketch block.
3591        let (sketch_block_ref, _) = self.mutate_ast(
3592            new_ast,
3593            sketch_id,
3594            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3595        )?;
3596        Ok(sketch_block_ref)
3597    }
3598
3599    async fn add_angle(
3600        &mut self,
3601        sketch: ObjectId,
3602        angle: Angle,
3603        new_ast: &mut ast::Node<ast::Program>,
3604    ) -> Result<AstNodeRef, KclError> {
3605        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3606            return Err(KclError::refactor(format!(
3607                "Angle constraint must have exactly 2 lines, got {}",
3608                angle.lines.len()
3609            )));
3610        };
3611        let sketch_id = sketch;
3612
3613        // Map the runtime objects back to variable names.
3614        let line0_object = self
3615            .scene_graph
3616            .objects
3617            .get(l0_id.0)
3618            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3619        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3620            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3621        };
3622        let Segment::Line(_) = line0_segment else {
3623            return Err(KclError::refactor(format!(
3624                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3625            )));
3626        };
3627        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3628
3629        let line1_object = self
3630            .scene_graph
3631            .objects
3632            .get(l1_id.0)
3633            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3634        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3635            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3636        };
3637        let Segment::Line(_) = line1_segment else {
3638            return Err(KclError::refactor(format!(
3639                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3640            )));
3641        };
3642        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3643
3644        // Create the angle() call.
3645        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3646            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3647            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3648                ast::ArrayExpression {
3649                    elements: vec![l0_ast, l1_ast],
3650                    digest: None,
3651                    non_code_meta: Default::default(),
3652                },
3653            )))),
3654            arguments: Default::default(),
3655            digest: None,
3656            non_code_meta: Default::default(),
3657        })));
3658        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3659            left: angle_call_ast,
3660            operator: ast::BinaryOperator::Eq,
3661            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3662                value: ast::LiteralValue::Number {
3663                    value: angle.angle.value,
3664                    suffix: angle.angle.units,
3665                },
3666                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3667                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3668                })?,
3669                digest: None,
3670            }))),
3671            digest: None,
3672        })));
3673
3674        // Add the line to the AST of the sketch block.
3675        let (sketch_block_ref, _) = self.mutate_ast(
3676            new_ast,
3677            sketch_id,
3678            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3679        )?;
3680        Ok(sketch_block_ref)
3681    }
3682
3683    async fn add_tangent(
3684        &mut self,
3685        sketch: ObjectId,
3686        tangent: Tangent,
3687        new_ast: &mut ast::Node<ast::Program>,
3688    ) -> Result<AstNodeRef, KclError> {
3689        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3690            return Err(KclError::refactor(format!(
3691                "Tangent constraint must have exactly 2 segments, got {}",
3692                tangent.input.len()
3693            )));
3694        };
3695        let sketch_id = sketch;
3696
3697        let seg0_object = self
3698            .scene_graph
3699            .objects
3700            .get(seg0_id.0)
3701            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3702        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3703            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3704        };
3705        let seg0_ast = match seg0_segment {
3706            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3707                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3708            }
3709            _ => {
3710                return Err(KclError::refactor(format!(
3711                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3712                )));
3713            }
3714        };
3715
3716        let seg1_object = self
3717            .scene_graph
3718            .objects
3719            .get(seg1_id.0)
3720            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3721        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3722            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3723        };
3724        let seg1_ast = match seg1_segment {
3725            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3726                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3727            }
3728            _ => {
3729                return Err(KclError::refactor(format!(
3730                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3731                )));
3732            }
3733        };
3734
3735        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3736        let (sketch_block_ref, _) = self.mutate_ast(
3737            new_ast,
3738            sketch_id,
3739            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3740        )?;
3741        Ok(sketch_block_ref)
3742    }
3743
3744    async fn add_symmetric(
3745        &mut self,
3746        sketch: ObjectId,
3747        symmetric: Symmetric,
3748        new_ast: &mut ast::Node<ast::Program>,
3749    ) -> Result<AstNodeRef, KclError> {
3750        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3751            return Err(KclError::refactor(format!(
3752                "Symmetric constraint must have exactly 2 inputs, got {}",
3753                symmetric.input.len()
3754            )));
3755        };
3756        let sketch_id = sketch;
3757
3758        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3759        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3760        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3761
3762        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3763        let (sketch_block_ref, _) = self.mutate_ast(
3764            new_ast,
3765            sketch_id,
3766            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3767        )?;
3768        Ok(sketch_block_ref)
3769    }
3770
3771    async fn add_midpoint(
3772        &mut self,
3773        sketch: ObjectId,
3774        midpoint: Midpoint,
3775        new_ast: &mut ast::Node<ast::Program>,
3776    ) -> Result<AstNodeRef, KclError> {
3777        let sketch_id = sketch;
3778        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3779
3780        let segment_object = self
3781            .scene_graph
3782            .objects
3783            .get(midpoint.segment.0)
3784            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3785        let ObjectKind::Segment {
3786            segment: midpoint_segment,
3787        } = &segment_object.kind
3788        else {
3789            return Err(KclError::refactor(format!(
3790                "Object must be a segment, but it was {}",
3791                segment_object.kind.human_friendly_kind_with_article()
3792            )));
3793        };
3794        let segment_ast = match midpoint_segment {
3795            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3796            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3797            _ => {
3798                return Err(KclError::refactor(format!(
3799                    "Midpoint target must be a line or arc segment but it was {}",
3800                    midpoint_segment.human_friendly_kind_with_article()
3801                )));
3802            }
3803        };
3804
3805        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3806        let (sketch_block_ref, _) = self.mutate_ast(
3807            new_ast,
3808            sketch_id,
3809            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3810        )?;
3811        Ok(sketch_block_ref)
3812    }
3813
3814    async fn add_equal_radius(
3815        &mut self,
3816        sketch: ObjectId,
3817        equal_radius: EqualRadius,
3818        new_ast: &mut ast::Node<ast::Program>,
3819    ) -> Result<AstNodeRef, KclError> {
3820        if equal_radius.input.len() < 2 {
3821            return Err(KclError::refactor(format!(
3822                "equalRadius constraint must have at least 2 segments, got {}",
3823                equal_radius.input.len()
3824            )));
3825        }
3826
3827        let sketch_id = sketch;
3828        let input_asts = equal_radius
3829            .input
3830            .iter()
3831            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3832            .collect::<Result<Vec<_>, _>>()?;
3833
3834        let equal_radius_ast = create_equal_radius_ast(input_asts);
3835        let (sketch_block_ref, _) = self.mutate_ast(
3836            new_ast,
3837            sketch_id,
3838            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3839        )?;
3840        Ok(sketch_block_ref)
3841    }
3842
3843    async fn add_radius(
3844        &mut self,
3845        sketch: ObjectId,
3846        radius: Radius,
3847        new_ast: &mut ast::Node<ast::Program>,
3848    ) -> Result<AstNodeRef, KclError> {
3849        let params = ArcSizeConstraintParams {
3850            points: vec![radius.arc],
3851            function_name: RADIUS_FN,
3852            value: radius.radius.value,
3853            units: radius.radius.units,
3854            label_position: radius.label_position,
3855            constraint_type_name: "Radius",
3856        };
3857        self.add_arc_size_constraint(sketch, params, new_ast).await
3858    }
3859
3860    async fn add_diameter(
3861        &mut self,
3862        sketch: ObjectId,
3863        diameter: Diameter,
3864        new_ast: &mut ast::Node<ast::Program>,
3865    ) -> Result<AstNodeRef, KclError> {
3866        let params = ArcSizeConstraintParams {
3867            points: vec![diameter.arc],
3868            function_name: DIAMETER_FN,
3869            value: diameter.diameter.value,
3870            units: diameter.diameter.units,
3871            label_position: diameter.label_position,
3872            constraint_type_name: "Diameter",
3873        };
3874        self.add_arc_size_constraint(sketch, params, new_ast).await
3875    }
3876
3877    async fn add_fixed_constraints(
3878        &mut self,
3879        sketch: ObjectId,
3880        points: Vec<FixedPoint>,
3881        new_ast: &mut ast::Node<ast::Program>,
3882    ) -> Result<AstNodeRef, KclError> {
3883        let mut sketch_block_ref = None;
3884
3885        for fixed_point in points {
3886            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3887            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3888                .map_err(|err| KclError::refactor(err.to_string()))?;
3889
3890            let (sketch_ref, _) = self.mutate_ast(
3891                new_ast,
3892                sketch,
3893                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3894            )?;
3895            sketch_block_ref = Some(sketch_ref);
3896        }
3897
3898        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3899    }
3900
3901    async fn add_arc_size_constraint(
3902        &mut self,
3903        sketch: ObjectId,
3904        params: ArcSizeConstraintParams,
3905        new_ast: &mut ast::Node<ast::Program>,
3906    ) -> Result<AstNodeRef, KclError> {
3907        let sketch_id = sketch;
3908
3909        // Constraint must have exactly 1 argument (arc segment)
3910        if params.points.len() != 1 {
3911            return Err(KclError::refactor(format!(
3912                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3913                params.constraint_type_name,
3914                params.points.len()
3915            )));
3916        }
3917
3918        let arc_id = params.points[0];
3919        let arc_object = self
3920            .scene_graph
3921            .objects
3922            .get(arc_id.0)
3923            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3924        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3925            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3926        };
3927        let ref_type = match arc_segment {
3928            Segment::Arc(_) => ARC_VARIABLE,
3929            Segment::Circle(_) => CIRCLE_VARIABLE,
3930            _ => {
3931                return Err(KclError::refactor(format!(
3932                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3933                    params.constraint_type_name
3934                )));
3935            }
3936        };
3937        // Reference the arc/circle segment directly
3938        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3939        let arguments = match &params.label_position {
3940            Some(label_position) => vec![ast::LabeledArg {
3941                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3942                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3943            }],
3944            None => Default::default(),
3945        };
3946
3947        // Create the function call.
3948        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3949            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3950            unlabeled: Some(arc_ast),
3951            arguments,
3952            digest: None,
3953            non_code_meta: Default::default(),
3954        })));
3955        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3956            left: call_ast,
3957            operator: ast::BinaryOperator::Eq,
3958            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3959                value: ast::LiteralValue::Number {
3960                    value: params.value,
3961                    suffix: params.units,
3962                },
3963                raw: format_number_literal(params.value, params.units, None)
3964                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3965                digest: None,
3966            }))),
3967            digest: None,
3968        })));
3969
3970        // Add the line to the AST of the sketch block.
3971        let (sketch_block_ref, _) = self.mutate_ast(
3972            new_ast,
3973            sketch_id,
3974            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3975        )?;
3976        Ok(sketch_block_ref)
3977    }
3978
3979    async fn add_horizontal_distance(
3980        &mut self,
3981        sketch: ObjectId,
3982        distance: Distance,
3983        new_ast: &mut ast::Node<ast::Program>,
3984    ) -> Result<AstNodeRef, KclError> {
3985        let sketch_id = sketch;
3986        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3987            [pt0, pt1] => [
3988                self.coincident_segment_to_ast(pt0, new_ast)?,
3989                self.coincident_segment_to_ast(pt1, new_ast)?,
3990            ],
3991            _ => {
3992                return Err(KclError::refactor(format!(
3993                    "Horizontal distance constraint must have exactly 2 points, got {}",
3994                    distance.points.len()
3995                )));
3996            }
3997        };
3998
3999        let arguments = match &distance.label_position {
4000            Some(label_position) => vec![ast::LabeledArg {
4001                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4002                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4003            }],
4004            None => Default::default(),
4005        };
4006
4007        // Create the horizontalDistance() call.
4008        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4009            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4010            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4011                ast::ArrayExpression {
4012                    elements: vec![pt0_ast, pt1_ast],
4013                    digest: None,
4014                    non_code_meta: Default::default(),
4015                },
4016            )))),
4017            arguments,
4018            digest: None,
4019            non_code_meta: Default::default(),
4020        })));
4021        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4022            left: distance_call_ast,
4023            operator: ast::BinaryOperator::Eq,
4024            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4025                value: ast::LiteralValue::Number {
4026                    value: distance.distance.value,
4027                    suffix: distance.distance.units,
4028                },
4029                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4030                    KclError::refactor(format!(
4031                        "Could not format numeric suffix: {:?}",
4032                        distance.distance.units
4033                    ))
4034                })?,
4035                digest: None,
4036            }))),
4037            digest: None,
4038        })));
4039
4040        // Add the line to the AST of the sketch block.
4041        let (sketch_block_ref, _) = self.mutate_ast(
4042            new_ast,
4043            sketch_id,
4044            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4045        )?;
4046        Ok(sketch_block_ref)
4047    }
4048
4049    async fn add_vertical_distance(
4050        &mut self,
4051        sketch: ObjectId,
4052        distance: Distance,
4053        new_ast: &mut ast::Node<ast::Program>,
4054    ) -> Result<AstNodeRef, KclError> {
4055        let sketch_id = sketch;
4056        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4057            [pt0, pt1] => [
4058                self.coincident_segment_to_ast(pt0, new_ast)?,
4059                self.coincident_segment_to_ast(pt1, new_ast)?,
4060            ],
4061            _ => {
4062                return Err(KclError::refactor(format!(
4063                    "Vertical distance constraint must have exactly 2 points, got {}",
4064                    distance.points.len()
4065                )));
4066            }
4067        };
4068
4069        let arguments = match &distance.label_position {
4070            Some(label_position) => vec![ast::LabeledArg {
4071                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4072                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4073            }],
4074            None => Default::default(),
4075        };
4076
4077        // Create the verticalDistance() call.
4078        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4079            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4080            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4081                ast::ArrayExpression {
4082                    elements: vec![pt0_ast, pt1_ast],
4083                    digest: None,
4084                    non_code_meta: Default::default(),
4085                },
4086            )))),
4087            arguments,
4088            digest: None,
4089            non_code_meta: Default::default(),
4090        })));
4091        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4092            left: distance_call_ast,
4093            operator: ast::BinaryOperator::Eq,
4094            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4095                value: ast::LiteralValue::Number {
4096                    value: distance.distance.value,
4097                    suffix: distance.distance.units,
4098                },
4099                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4100                    KclError::refactor(format!(
4101                        "Could not format numeric suffix: {:?}",
4102                        distance.distance.units
4103                    ))
4104                })?,
4105                digest: None,
4106            }))),
4107            digest: None,
4108        })));
4109
4110        // Add the line to the AST of the sketch block.
4111        let (sketch_block_ref, _) = self.mutate_ast(
4112            new_ast,
4113            sketch_id,
4114            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4115        )?;
4116        Ok(sketch_block_ref)
4117    }
4118
4119    async fn add_horizontal(
4120        &mut self,
4121        sketch: ObjectId,
4122        horizontal: Horizontal,
4123        new_ast: &mut ast::Node<ast::Program>,
4124    ) -> Result<AstNodeRef, KclError> {
4125        let sketch_id = sketch;
4126
4127        // Map the runtime objects back to variable names.
4128        let first_arg_ast = match horizontal {
4129            Horizontal::Line { line } => {
4130                let line_object = self
4131                    .scene_graph
4132                    .objects
4133                    .get(line.0)
4134                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4135                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4136                    let kind = line_object.kind.human_friendly_kind_with_article();
4137                    return Err(KclError::refactor(format!(
4138                        "This constraint only works on Segments, but you selected {kind}"
4139                    )));
4140                };
4141                let Segment::Line(_) = line_segment else {
4142                    return Err(KclError::refactor(format!(
4143                        "Only lines can be made horizontal, but you selected {}",
4144                        line_segment.human_friendly_kind_with_article(),
4145                    )));
4146                };
4147                self.line_id_to_ast_reference(line, new_ast)?
4148            }
4149            Horizontal::Points { points } => {
4150                let point_asts = points
4151                    .iter()
4152                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4153                    .collect::<Result<Vec<_>, _>>()?;
4154                ast::ArrayExpression::new(point_asts).into()
4155            }
4156        };
4157        // Create the horizontal() call using shared helper.
4158        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4159
4160        // Add the line to the AST of the sketch block.
4161        let (sketch_block_ref, _) = self.mutate_ast(
4162            new_ast,
4163            sketch_id,
4164            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4165        )?;
4166        Ok(sketch_block_ref)
4167    }
4168
4169    async fn add_lines_equal_length(
4170        &mut self,
4171        sketch: ObjectId,
4172        lines_equal_length: LinesEqualLength,
4173        new_ast: &mut ast::Node<ast::Program>,
4174    ) -> Result<AstNodeRef, KclError> {
4175        if lines_equal_length.lines.len() < 2 {
4176            return Err(KclError::refactor(format!(
4177                "Lines equal length constraint must have at least 2 lines, got {}",
4178                lines_equal_length.lines.len()
4179            )));
4180        };
4181
4182        let sketch_id = sketch;
4183
4184        // Map the runtime objects back to variable names.
4185        let line_asts = lines_equal_length
4186            .lines
4187            .iter()
4188            .map(|line_id| {
4189                let line_object = self
4190                    .scene_graph
4191                    .objects
4192                    .get(line_id.0)
4193                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4194                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4195                    let kind = line_object.kind.human_friendly_kind_with_article();
4196                    return Err(KclError::refactor(format!(
4197                        "This constraint only works on Segments, but you selected {kind}"
4198                    )));
4199                };
4200                let Segment::Line(_) = line_segment else {
4201                    let kind = line_segment.human_friendly_kind_with_article();
4202                    return Err(KclError::refactor(format!(
4203                        "Only lines can be made equal length, but you selected {kind}"
4204                    )));
4205                };
4206
4207                self.line_id_to_ast_reference(*line_id, new_ast)
4208            })
4209            .collect::<Result<Vec<_>, _>>()?;
4210
4211        // Create the equalLength() call using shared helper.
4212        let equal_length_ast = create_equal_length_ast(line_asts);
4213
4214        // Add the constraint to the AST of the sketch block.
4215        let (sketch_block_ref, _) = self.mutate_ast(
4216            new_ast,
4217            sketch_id,
4218            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4219        )?;
4220        Ok(sketch_block_ref)
4221    }
4222
4223    fn equal_radius_segment_id_to_ast_reference(
4224        &mut self,
4225        segment_id: ObjectId,
4226        new_ast: &mut ast::Node<ast::Program>,
4227    ) -> Result<ast::Expr, KclError> {
4228        let segment_object = self
4229            .scene_graph
4230            .objects
4231            .get(segment_id.0)
4232            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4233        let ObjectKind::Segment { segment } = &segment_object.kind else {
4234            return Err(KclError::refactor(format!(
4235                "Object is not a segment, it was {}",
4236                segment_object.kind.human_friendly_kind_with_article()
4237            )));
4238        };
4239
4240        let ref_type = match segment {
4241            Segment::Arc(_) => ARC_VARIABLE,
4242            Segment::Circle(_) => CIRCLE_VARIABLE,
4243            _ => {
4244                return Err(KclError::refactor(format!(
4245                    "equalRadius supports only arc/circle segments, got {}",
4246                    segment.human_friendly_kind_with_article()
4247                )));
4248            }
4249        };
4250
4251        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4252    }
4253
4254    fn symmetric_input_id_to_ast_reference(
4255        &mut self,
4256        segment_id: ObjectId,
4257        new_ast: &mut ast::Node<ast::Program>,
4258    ) -> Result<ast::Expr, KclError> {
4259        let segment_object = self
4260            .scene_graph
4261            .objects
4262            .get(segment_id.0)
4263            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4264        let ObjectKind::Segment { segment } = &segment_object.kind else {
4265            return Err(KclError::refactor(format!(
4266                "Object is not a segment, it was {}",
4267                segment_object.kind.human_friendly_kind_with_article()
4268            )));
4269        };
4270
4271        match segment {
4272            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4273            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4274            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4275            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4276            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4277                "Symmetric does not yet support control point splines".to_owned(),
4278            )),
4279        }
4280    }
4281
4282    fn symmetric_axis_id_to_ast_reference(
4283        &mut self,
4284        segment_id: ObjectId,
4285        new_ast: &mut ast::Node<ast::Program>,
4286    ) -> Result<ast::Expr, KclError> {
4287        let segment_object = self
4288            .scene_graph
4289            .objects
4290            .get(segment_id.0)
4291            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4292        let ObjectKind::Segment { segment } = &segment_object.kind else {
4293            return Err(KclError::refactor(format!(
4294                "Object is not a segment, it was {}",
4295                segment_object.kind.human_friendly_kind_with_article()
4296            )));
4297        };
4298        match segment {
4299            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4300            _ => Err(KclError::refactor(format!(
4301                "Symmetric axis must be a line, got {}",
4302                segment.human_friendly_kind_with_article()
4303            ))),
4304        }
4305    }
4306
4307    async fn add_parallel(
4308        &mut self,
4309        sketch: ObjectId,
4310        parallel: Parallel,
4311        new_ast: &mut ast::Node<ast::Program>,
4312    ) -> Result<AstNodeRef, KclError> {
4313        if parallel.lines.len() < 2 {
4314            return Err(KclError::refactor(format!(
4315                "Parallel constraint must have at least 2 lines, got {}",
4316                parallel.lines.len()
4317            )));
4318        };
4319
4320        let sketch_id = sketch;
4321
4322        let line_asts = parallel
4323            .lines
4324            .iter()
4325            .map(|line_id| {
4326                let line_object = self
4327                    .scene_graph
4328                    .objects
4329                    .get(line_id.0)
4330                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332                    let kind = line_object.kind.human_friendly_kind_with_article();
4333                    return Err(KclError::refactor(format!(
4334                        "This constraint only works on Segments, but you selected {kind}"
4335                    )));
4336                };
4337                let Segment::Line(_) = line_segment else {
4338                    let kind = line_segment.human_friendly_kind_with_article();
4339                    return Err(KclError::refactor(format!(
4340                        "Only lines can be made parallel, but you selected {kind}"
4341                    )));
4342                };
4343
4344                self.line_id_to_ast_reference(*line_id, new_ast)
4345            })
4346            .collect::<Result<Vec<_>, _>>()?;
4347
4348        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4349            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4350            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4351                ast::ArrayExpression {
4352                    elements: line_asts,
4353                    digest: None,
4354                    non_code_meta: Default::default(),
4355                },
4356            )))),
4357            arguments: Default::default(),
4358            digest: None,
4359            non_code_meta: Default::default(),
4360        })));
4361
4362        let (sketch_block_ref, _) = self.mutate_ast(
4363            new_ast,
4364            sketch_id,
4365            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4366        )?;
4367        Ok(sketch_block_ref)
4368    }
4369
4370    async fn add_perpendicular(
4371        &mut self,
4372        sketch: ObjectId,
4373        perpendicular: Perpendicular,
4374        new_ast: &mut ast::Node<ast::Program>,
4375    ) -> Result<AstNodeRef, KclError> {
4376        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4377            .await
4378    }
4379
4380    async fn add_lines_at_angle_constraint(
4381        &mut self,
4382        sketch: ObjectId,
4383        angle_kind: LinesAtAngleKind,
4384        lines: Vec<ObjectId>,
4385        new_ast: &mut ast::Node<ast::Program>,
4386    ) -> Result<AstNodeRef, KclError> {
4387        let &[line0_id, line1_id] = lines.as_slice() else {
4388            return Err(KclError::refactor(format!(
4389                "{} constraint must have exactly 2 lines, got {}",
4390                angle_kind.to_function_name(),
4391                lines.len()
4392            )));
4393        };
4394
4395        let sketch_id = sketch;
4396
4397        // Map the runtime objects back to variable names.
4398        let line0_object = self
4399            .scene_graph
4400            .objects
4401            .get(line0_id.0)
4402            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4403        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4404            let kind = line0_object.kind.human_friendly_kind_with_article();
4405            return Err(KclError::refactor(format!(
4406                "This constraint only works on Segments, but you selected {kind}"
4407            )));
4408        };
4409        let Segment::Line(_) = line0_segment else {
4410            return Err(KclError::refactor(format!(
4411                "Only lines can be made {}, but you selected {}",
4412                angle_kind.to_function_name(),
4413                line0_segment.human_friendly_kind_with_article(),
4414            )));
4415        };
4416        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4417
4418        let line1_object = self
4419            .scene_graph
4420            .objects
4421            .get(line1_id.0)
4422            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4423        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4424            let kind = line1_object.kind.human_friendly_kind_with_article();
4425            return Err(KclError::refactor(format!(
4426                "This constraint only works on Segments, but you selected {kind}"
4427            )));
4428        };
4429        let Segment::Line(_) = line1_segment else {
4430            return Err(KclError::refactor(format!(
4431                "Only lines can be made {}, but you selected {}",
4432                angle_kind.to_function_name(),
4433                line1_segment.human_friendly_kind_with_article(),
4434            )));
4435        };
4436        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4437
4438        // Create the parallel() or perpendicular() call.
4439        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4440            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4441            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4442                ast::ArrayExpression {
4443                    elements: vec![line0_ast, line1_ast],
4444                    digest: None,
4445                    non_code_meta: Default::default(),
4446                },
4447            )))),
4448            arguments: Default::default(),
4449            digest: None,
4450            non_code_meta: Default::default(),
4451        })));
4452
4453        // Add the constraint to the AST of the sketch block.
4454        let (sketch_block_ref, _) = self.mutate_ast(
4455            new_ast,
4456            sketch_id,
4457            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4458        )?;
4459        Ok(sketch_block_ref)
4460    }
4461
4462    async fn add_vertical(
4463        &mut self,
4464        sketch: ObjectId,
4465        vertical: Vertical,
4466        new_ast: &mut ast::Node<ast::Program>,
4467    ) -> Result<AstNodeRef, KclError> {
4468        let sketch_id = sketch;
4469
4470        let first_arg_ast = match vertical {
4471            Vertical::Line { line } => {
4472                // Map the runtime objects back to variable names.
4473                let line_object = self
4474                    .scene_graph
4475                    .objects
4476                    .get(line.0)
4477                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4478                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4479                    let kind = line_object.kind.human_friendly_kind_with_article();
4480                    return Err(KclError::refactor(format!(
4481                        "This constraint only works on Segments, but you selected {kind}"
4482                    )));
4483                };
4484                let Segment::Line(_) = line_segment else {
4485                    return Err(KclError::refactor(format!(
4486                        "Only lines can be made vertical, but you selected {}",
4487                        line_segment.human_friendly_kind_with_article()
4488                    )));
4489                };
4490                self.line_id_to_ast_reference(line, new_ast)?
4491            }
4492            Vertical::Points { points } => {
4493                let point_asts = points
4494                    .iter()
4495                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4496                    .collect::<Result<Vec<_>, _>>()?;
4497                ast::ArrayExpression::new(point_asts).into()
4498            }
4499        };
4500        // Create the vertical() call using shared helper.
4501        let vertical_ast = create_vertical_ast(first_arg_ast);
4502
4503        // Add the line to the AST of the sketch block.
4504        let (sketch_block_ref, _) = self.mutate_ast(
4505            new_ast,
4506            sketch_id,
4507            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4508        )?;
4509        Ok(sketch_block_ref)
4510    }
4511
4512    async fn execute_after_add_constraint(
4513        &mut self,
4514        ctx: &ExecutorContext,
4515        sketch_id: ObjectId,
4516        sketch_block_ref: AstNodeRef,
4517        new_ast: &mut ast::Node<ast::Program>,
4518    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4519        // Convert to string source to create real source ranges.
4520        let new_source = source_from_ast(new_ast);
4521        // Parse the new KCL source.
4522        let (new_program, errors) = Program::parse(&new_source)
4523            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4524        if !errors.is_empty() {
4525            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4526                "Error parsing KCL source after adding constraint: {errors:?}"
4527            ))));
4528        }
4529        let Some(new_program) = new_program else {
4530            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4531                "No AST produced after adding constraint".to_string(),
4532            )));
4533        };
4534        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4535            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4536                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4537            )))
4538        })?;
4539
4540        // Truncate after the sketch block for mock execution.
4541        // Use a clone so we don't mutate new_program yet
4542        let mut truncated_program = new_program.clone();
4543        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4544            .map_err(KclErrorWithOutputs::no_outputs)?;
4545
4546        // Execute - if this fails, we haven't modified self yet, so state is safe
4547        let outcome = ctx
4548            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4549            .await?;
4550
4551        let new_object_ids = {
4552            // Extract the constraint ID from the execution outcome using source_range_to_object
4553            let constraint_id = outcome
4554                .source_range_to_object
4555                .get(&constraint_node_ref.range)
4556                .copied()
4557                .ok_or_else(|| {
4558                    KclErrorWithOutputs::from_error_outcome(
4559                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4560                        outcome.clone(),
4561                    )
4562                })?;
4563            vec![constraint_id]
4564        };
4565
4566        // Only now, after all operations succeeded, update self.program
4567        // This ensures state is only modified if everything succeeds
4568        self.program = new_program;
4569
4570        // Uses MockConfig::default() which has freedom_analysis: true
4571        let outcome = self.update_state_after_exec(outcome, true);
4572
4573        let src_delta = SourceDelta { text: new_source };
4574        let scene_graph_delta = SceneGraphDelta {
4575            new_graph: self.scene_graph_for_ui(),
4576            invalidates_ids: false,
4577            new_objects: new_object_ids,
4578            exec_outcome: outcome,
4579        };
4580        Ok((src_delta, scene_graph_delta))
4581    }
4582
4583    // Find constraints that reference the given segments.
4584    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4585        if segment_ids_set.contains(&segment_id) {
4586            return true;
4587        }
4588
4589        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4590            return false;
4591        };
4592        let ObjectKind::Segment { segment } = &segment_object.kind else {
4593            return false;
4594        };
4595        let Segment::Point(point) = segment else {
4596            return false;
4597        };
4598
4599        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4600    }
4601
4602    fn remaining_constraint_segments(
4603        &self,
4604        segments: &[ConstraintSegment],
4605        segment_ids_set: &AhashIndexSet<ObjectId>,
4606    ) -> Vec<ConstraintSegment> {
4607        segments
4608            .iter()
4609            .copied()
4610            .filter(|segment| match segment {
4611                ConstraintSegment::Origin(_) => true,
4612                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4613            })
4614            .collect()
4615    }
4616
4617    fn find_referenced_constraints(
4618        &self,
4619        sketch_id: ObjectId,
4620        segment_ids_set: &AhashIndexSet<ObjectId>,
4621    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4622        // Look up the sketch.
4623        let sketch_object = self
4624            .scene_graph
4625            .objects
4626            .get(sketch_id.0)
4627            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4628        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4629            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4630        };
4631        let segment_or_owner_matches = |segment_id: ObjectId| {
4632            if segment_ids_set.contains(&segment_id) {
4633                return true;
4634            }
4635            let segment_object = self.scene_graph.objects.get(segment_id.0);
4636            if let Some(obj) = segment_object
4637                && let ObjectKind::Segment { segment } = &obj.kind
4638            {
4639                match segment {
4640                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4641                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4642                    _ => false,
4643                }
4644            } else {
4645                false
4646            }
4647        };
4648        let mut constraint_ids_set = AhashIndexSet::default();
4649        for constraint_id in &sketch.constraints {
4650            let constraint_object = self
4651                .scene_graph
4652                .objects
4653                .get(constraint_id.0)
4654                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4655            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4656                return Err(KclError::refactor(format!(
4657                    "Object is not a constraint, it is {}",
4658                    constraint_object.kind.human_friendly_kind_with_article()
4659                )));
4660            };
4661            let depends_on_segment = match constraint {
4662                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4663                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4664                Constraint::Fixed(fixed) => fixed
4665                    .points
4666                    .iter()
4667                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4668                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4669                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4670                Constraint::EqualRadius(equal_radius) => {
4671                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4672                }
4673                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4674                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4675                Constraint::Horizontal(h) => match h {
4676                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4677                    Horizontal::Points { points } => points.iter().any(|point| match point {
4678                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4679                        ConstraintSegment::Origin(_) => false,
4680                    }),
4681                },
4682                Constraint::Vertical(v) => match v {
4683                    Vertical::Line { line } => segment_or_owner_matches(*line),
4684                    Vertical::Points { points } => points.iter().any(|point| match point {
4685                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4686                        ConstraintSegment::Origin(_) => false,
4687                    }),
4688                },
4689                Constraint::LinesEqualLength(lines_equal_length) => {
4690                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4691                }
4692                Constraint::Midpoint(midpoint) => {
4693                    segment_or_owner_matches(midpoint.segment) || segment_or_owner_matches(midpoint.point)
4694                }
4695                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4696                Constraint::Perpendicular(perpendicular) => {
4697                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4698                }
4699                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4700                Constraint::Symmetric(symmetric) => {
4701                    segment_or_owner_matches(symmetric.axis)
4702                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4703                }
4704                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4705            };
4706            if depends_on_segment {
4707                constraint_ids_set.insert(*constraint_id);
4708            }
4709        }
4710        Ok(constraint_ids_set)
4711    }
4712
4713    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4714        let mut outcome = outcome;
4715        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4716
4717        if freedom_analysis_ran {
4718            // When freedom analysis ran, replace the cache entirely with new values
4719            // Don't merge with old values since IDs might have changed
4720            self.point_freedom_cache.clear();
4721            for new_obj in &new_objects {
4722                if let ObjectKind::Segment {
4723                    segment: crate::front::Segment::Point(point),
4724                } = &new_obj.kind
4725                {
4726                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4727                }
4728            }
4729            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4730            // Objects are already correct from the analysis, just use them as-is
4731            self.scene_graph.objects = new_objects;
4732        } else {
4733            // When freedom analysis didn't run, preserve old values and merge
4734            // Before replacing objects, extract and store freedom values from old objects
4735            for old_obj in &self.scene_graph.objects {
4736                if let ObjectKind::Segment {
4737                    segment: crate::front::Segment::Point(point),
4738                } = &old_obj.kind
4739                {
4740                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4741                }
4742            }
4743
4744            // Update objects, preserving stored freedom values when new is Free (might be default)
4745            let mut updated_objects = Vec::with_capacity(new_objects.len());
4746            for new_obj in new_objects {
4747                let mut obj = new_obj;
4748                if let ObjectKind::Segment {
4749                    segment: crate::front::Segment::Point(point),
4750                } = &mut obj.kind
4751                {
4752                    let new_freedom = point.freedom;
4753                    // When freedom_analysis=false, new values are defaults (Free).
4754                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4755                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4756                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4757                    // when there are actually unsatisfied constraints.
4758                    match new_freedom {
4759                        Freedom::Free => {
4760                            match self.point_freedom_cache.get(&obj.id).copied() {
4761                                Some(Freedom::Conflict) => {
4762                                    // Don't preserve Conflict - conflicts are transient
4763                                    // Keep it as Free
4764                                }
4765                                Some(Freedom::Fixed) => {
4766                                    // Preserve Fixed cached value
4767                                    point.freedom = Freedom::Fixed;
4768                                }
4769                                Some(Freedom::Free) => {
4770                                    // If stored is also Free, keep Free (no change needed)
4771                                }
4772                                None => {
4773                                    // If no cached value, keep Free (default)
4774                                }
4775                            }
4776                        }
4777                        Freedom::Fixed => {
4778                            // Use new value (already set)
4779                        }
4780                        Freedom::Conflict => {
4781                            // Use new value (already set)
4782                        }
4783                    }
4784                    // Store the new freedom value (even if it's Free, so we know it was set)
4785                    self.point_freedom_cache.insert(obj.id, point.freedom);
4786                }
4787                updated_objects.push(obj);
4788            }
4789
4790            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4791            self.scene_graph.objects = updated_objects;
4792        }
4793        outcome
4794    }
4795
4796    fn mutate_ast(
4797        &mut self,
4798        ast: &mut ast::Node<ast::Program>,
4799        object_id: ObjectId,
4800        command: AstMutateCommand,
4801    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4802        let sketch_object = self
4803            .scene_graph
4804            .objects
4805            .get(object_id.0)
4806            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4807        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4808    }
4809}
4810
4811fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4812    // Look up existing sketch.
4813    let sketch_object = scene_graph
4814        .objects
4815        .get(sketch_id.0)
4816        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4817    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4818        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4819    };
4820    expect_single_node_ref(sketch_object)
4821}
4822
4823fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4824    match &object.source {
4825        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4826            range: *range,
4827            node_path: node_path.clone(),
4828        }),
4829        SourceRef::BackTrace { ranges } => {
4830            let [range] = ranges.as_slice() else {
4831                return Err(KclError::refactor(format!(
4832                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4833                    ranges.len()
4834                )));
4835            };
4836            Ok(AstNodeRef {
4837                range: range.0,
4838                node_path: range.1.clone(),
4839            })
4840        }
4841    }
4842}
4843
4844/// This is a deprecated fall-back implementation. Prefer
4845/// [`only_sketch_block()`] to avoid reliance on source ranges.
4846fn only_sketch_block_from_range(
4847    ast: &mut ast::Node<ast::Program>,
4848    sketch_block_range: SourceRange,
4849    edit_kind: ChangeKind,
4850) -> Result<(), KclError> {
4851    let r1 = sketch_block_range;
4852    let matches_range = |r2: SourceRange| -> bool {
4853        // We may have added items to the sketch block, so the end may not be an
4854        // exact match.
4855        match edit_kind {
4856            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4857            // For edit, we don't know whether it grew or shrank.
4858            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4859            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4860            // No edit should be an exact match.
4861            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4862        }
4863    };
4864    let mut found = false;
4865    for item in ast.body.iter_mut() {
4866        match item {
4867            ast::BodyItem::ImportStatement(_) => {}
4868            ast::BodyItem::ExpressionStatement(node) => {
4869                if matches_range(SourceRange::from(&*node))
4870                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4871                {
4872                    sketch_block.is_being_edited = true;
4873                    found = true;
4874                    break;
4875                }
4876            }
4877            ast::BodyItem::VariableDeclaration(node) => {
4878                if matches_range(SourceRange::from(&node.declaration.init))
4879                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4880                {
4881                    sketch_block.is_being_edited = true;
4882                    found = true;
4883                    break;
4884                }
4885            }
4886            ast::BodyItem::TypeDeclaration(_) => {}
4887            ast::BodyItem::ReturnStatement(node) => {
4888                if matches_range(SourceRange::from(&node.argument))
4889                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4890                {
4891                    sketch_block.is_being_edited = true;
4892                    found = true;
4893                    break;
4894                }
4895            }
4896        }
4897    }
4898    if !found {
4899        return Err(KclError::refactor(format!(
4900            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4901        )));
4902    }
4903
4904    Ok(())
4905}
4906
4907fn only_sketch_block(
4908    ast: &mut ast::Node<ast::Program>,
4909    sketch_block_ref: &AstNodeRef,
4910    edit_kind: ChangeKind,
4911) -> Result<(), KclError> {
4912    let Some(target_node_path) = &sketch_block_ref.node_path else {
4913        #[cfg(target_arch = "wasm32")]
4914        web_sys::console::warn_1(
4915            &format!(
4916                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4917                &sketch_block_ref
4918            )
4919            .into(),
4920        );
4921        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4922    };
4923    let mut found = false;
4924    for item in ast.body.iter_mut() {
4925        match item {
4926            ast::BodyItem::ImportStatement(_) => {}
4927            ast::BodyItem::ExpressionStatement(node) => {
4928                // Check the statement.
4929                if let Some(node_path) = &node.node_path
4930                    && node_path == target_node_path
4931                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4932                {
4933                    sketch_block.is_being_edited = true;
4934                    found = true;
4935                    break;
4936                }
4937                // Check the expression.
4938                if let Some(node_path) = node.expression.node_path()
4939                    && node_path == target_node_path
4940                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4941                {
4942                    sketch_block.is_being_edited = true;
4943                    found = true;
4944                    break;
4945                }
4946            }
4947            ast::BodyItem::VariableDeclaration(node) => {
4948                if let Some(node_path) = node.declaration.init.node_path()
4949                    && node_path == target_node_path
4950                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4951                {
4952                    sketch_block.is_being_edited = true;
4953                    found = true;
4954                    break;
4955                }
4956            }
4957            ast::BodyItem::TypeDeclaration(_) => {}
4958            ast::BodyItem::ReturnStatement(node) => {
4959                if let Some(node_path) = node.argument.node_path()
4960                    && node_path == target_node_path
4961                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4962                {
4963                    sketch_block.is_being_edited = true;
4964                    found = true;
4965                    break;
4966                }
4967            }
4968        }
4969    }
4970    if !found {
4971        return Err(KclError::refactor(format!(
4972            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4973        )));
4974    }
4975
4976    Ok(())
4977}
4978
4979fn sketch_on_ast_expr(
4980    ast: &mut ast::Node<ast::Program>,
4981    scene_graph: &SceneGraph,
4982    on: &Plane,
4983) -> Result<ast::Expr, KclError> {
4984    match on {
4985        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4986        Plane::Object(object_id) => {
4987            let on_object = scene_graph
4988                .objects
4989                .get(object_id.0)
4990                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4991            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4992                return Ok(face_expr);
4993            }
4994            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4995        }
4996    }
4997}
4998
4999fn sketch_face_of_scene_object_ast_expr(
5000    ast: &mut ast::Node<ast::Program>,
5001    on_object: &crate::front::Object,
5002) -> Result<Option<ast::Expr>, KclError> {
5003    let SourceRef::BackTrace { ranges } = &on_object.source else {
5004        return Ok(None);
5005    };
5006
5007    match &on_object.kind {
5008        ObjectKind::Wall(_) => {
5009            let [sweep_range, segment_range] = ranges.as_slice() else {
5010                return Err(KclError::refactor(format!(
5011                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5012                    ranges.len(),
5013                    on_object.artifact_id
5014                )));
5015            };
5016            let sweep_ref = get_or_insert_ast_reference(
5017                ast,
5018                &SourceRef::Simple {
5019                    range: sweep_range.0,
5020                    node_path: sweep_range.1.clone(),
5021                },
5022                "solid",
5023                None,
5024            )?;
5025            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5026                return Err(KclError::refactor(format!(
5027                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5028                    on_object.artifact_id
5029                )));
5030            };
5031            let solid_name = solid_name_expr.name.name.clone();
5032            let solid_expr = ast_name_expr(solid_name.clone());
5033            let segment_ref = get_or_insert_ast_reference(
5034                ast,
5035                &SourceRef::Simple {
5036                    range: segment_range.0,
5037                    node_path: segment_range.1.clone(),
5038                },
5039                LINE_VARIABLE,
5040                None,
5041            )?;
5042
5043            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5044                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5045                    return Err(KclError::refactor(format!(
5046                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5047                        on_object.artifact_id
5048                    )));
5049                };
5050                create_member_expression(
5051                    create_member_expression(ast_name_expr(region_name), "tags"),
5052                    &segment_name_expr.name.name,
5053                )
5054            } else {
5055                segment_ref
5056            };
5057
5058            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5059        }
5060        ObjectKind::Cap(cap) => {
5061            let [range] = ranges.as_slice() else {
5062                return Err(KclError::refactor(format!(
5063                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5064                    ranges.len(),
5065                    on_object.artifact_id
5066                )));
5067            };
5068            let sweep_ref = get_or_insert_ast_reference(
5069                ast,
5070                &SourceRef::Simple {
5071                    range: range.0,
5072                    node_path: range.1.clone(),
5073                },
5074                "solid",
5075                None,
5076            )?;
5077            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5078                return Err(KclError::refactor(format!(
5079                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5080                    on_object.artifact_id
5081                )));
5082            };
5083            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5084            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5085            let face_expr = match cap.kind {
5086                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5087                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5088            };
5089
5090            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5091        }
5092        _ => Ok(None),
5093    }
5094}
5095
5096fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5097    let mut existing_artifact_ids = scene_objects
5098        .iter()
5099        .map(|object| object.artifact_id)
5100        .collect::<HashSet<_>>();
5101
5102    for artifact in artifact_graph.values() {
5103        match artifact {
5104            Artifact::Wall(wall) => {
5105                if existing_artifact_ids.contains(&wall.id) {
5106                    continue;
5107                }
5108
5109                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5110                    Artifact::Segment(segment) => Some(segment),
5111                    _ => None,
5112                }) else {
5113                    continue;
5114                };
5115                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5116                    Artifact::Sweep(sweep) => Some(sweep),
5117                    _ => None,
5118                }) else {
5119                    continue;
5120                };
5121                let source_segment = segment
5122                    .original_seg_id
5123                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5124                    .and_then(|artifact| match artifact {
5125                        Artifact::Segment(segment) => Some(segment),
5126                        _ => None,
5127                    })
5128                    .unwrap_or(segment);
5129                let id = ObjectId(scene_objects.len());
5130                scene_objects.push(crate::front::Object {
5131                    id,
5132                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5133                    label: Default::default(),
5134                    comments: Default::default(),
5135                    artifact_id: wall.id,
5136                    source: SourceRef::BackTrace {
5137                        ranges: vec![
5138                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5139                            (
5140                                source_segment.code_ref.range,
5141                                Some(source_segment.code_ref.node_path.clone()),
5142                            ),
5143                        ],
5144                    },
5145                });
5146                existing_artifact_ids.insert(wall.id);
5147            }
5148            Artifact::Cap(cap) => {
5149                if existing_artifact_ids.contains(&cap.id) {
5150                    continue;
5151                }
5152
5153                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5154                    Artifact::Sweep(sweep) => Some(sweep),
5155                    _ => None,
5156                }) else {
5157                    continue;
5158                };
5159                let id = ObjectId(scene_objects.len());
5160                let kind = match cap.sub_type {
5161                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5162                    CapSubType::End => crate::frontend::api::CapKind::End,
5163                };
5164                scene_objects.push(crate::front::Object {
5165                    id,
5166                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5167                    label: Default::default(),
5168                    comments: Default::default(),
5169                    artifact_id: cap.id,
5170                    source: SourceRef::BackTrace {
5171                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5172                    },
5173                });
5174                existing_artifact_ids.insert(cap.id);
5175            }
5176            _ => {}
5177        }
5178    }
5179}
5180
5181fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5182    use crate::engine::PlaneName;
5183
5184    match name {
5185        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5186        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5187        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5188        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5189        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5190        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5191    }
5192}
5193
5194fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5195    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5196        ast::UnaryOperator::Neg,
5197        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5198    )))
5199}
5200
5201fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5202    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5203        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5204        unlabeled: Some(solid_expr),
5205        arguments: vec![ast::LabeledArg {
5206            label: Some(ast::Identifier::new("face")),
5207            arg: face_expr,
5208        }],
5209        digest: None,
5210        non_code_meta: Default::default(),
5211    })))
5212}
5213
5214fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5215    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5216        return None;
5217    };
5218    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5219        return None;
5220    };
5221    if !matches!(
5222        sweep_call.callee.name.name.as_str(),
5223        "extrude" | "revolve" | "sweep" | "loft"
5224    ) {
5225        return None;
5226    }
5227    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5228        return None;
5229    };
5230    let candidate = region_name_expr.name.name.clone();
5231    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5232        return None;
5233    };
5234    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5235        return None;
5236    };
5237    if region_call.callee.name.name != "region" {
5238        return None;
5239    }
5240    Some(candidate)
5241}
5242
5243/// Return the AST expression referencing the variable at the given source ref.
5244/// If no such variable exists, insert a new variable declaration with the given
5245/// prefix.
5246///
5247/// This may return a complex expression referencing properties of the variable
5248/// (e.g., `line1.start`).
5249fn get_or_insert_ast_reference(
5250    ast: &mut ast::Node<ast::Program>,
5251    source_ref: &SourceRef,
5252    prefix: &str,
5253    property: Option<&str>,
5254) -> Result<ast::Expr, KclError> {
5255    let command = AstMutateCommand::AddVariableDeclaration {
5256        prefix: prefix.to_owned(),
5257    };
5258    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5259    let AstMutateCommandReturn::Name(var_name) = ret else {
5260        return Err(KclError::refactor(
5261            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5262        ));
5263    };
5264    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5265    let Some(property) = property else {
5266        // No property; just return the variable name.
5267        return Ok(var_expr);
5268    };
5269
5270    Ok(create_member_expression(var_expr, property))
5271}
5272
5273fn mutate_ast_node_by_source_ref(
5274    ast: &mut ast::Node<ast::Program>,
5275    source_ref: &SourceRef,
5276    command: AstMutateCommand,
5277) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5278    let (source_range, node_path) = match source_ref {
5279        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5280        SourceRef::BackTrace { ranges } => {
5281            let [range] = ranges.as_slice() else {
5282                return Err(KclError::refactor(format!(
5283                    "Expected single source ref, got {}; ranges={ranges:#?}",
5284                    ranges.len(),
5285                )));
5286            };
5287            (range.0, range.1.clone())
5288        }
5289    };
5290    let mut context = AstMutateContext {
5291        source_range,
5292        node_path,
5293        command,
5294        defined_names_stack: Default::default(),
5295    };
5296    let control = dfs_mut(ast, &mut context);
5297    match control {
5298        ControlFlow::Continue(_) => Err(KclError::refactor(
5299            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5300        )),
5301        ControlFlow::Break(break_value) => break_value,
5302    }
5303}
5304
5305#[derive(Debug)]
5306struct AstMutateContext {
5307    source_range: SourceRange,
5308    node_path: Option<ast::NodePath>,
5309    command: AstMutateCommand,
5310    defined_names_stack: Vec<HashSet<String>>,
5311}
5312
5313#[derive(Debug)]
5314#[allow(clippy::large_enum_variant)]
5315enum AstMutateCommand {
5316    /// Add an expression statement to the sketch block.
5317    AddSketchBlockExprStmt {
5318        expr: ast::Expr,
5319    },
5320    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5321    AddSketchBlockVarDecl {
5322        prefix: String,
5323        expr: ast::Expr,
5324    },
5325    AddVariableDeclaration {
5326        prefix: String,
5327    },
5328    EditPoint {
5329        at: ast::Expr,
5330    },
5331    EditLine {
5332        start: ast::Expr,
5333        end: ast::Expr,
5334        construction: Option<bool>,
5335    },
5336    EditArc {
5337        start: ast::Expr,
5338        end: ast::Expr,
5339        center: ast::Expr,
5340        construction: Option<bool>,
5341    },
5342    EditCircle {
5343        start: ast::Expr,
5344        center: ast::Expr,
5345        construction: Option<bool>,
5346    },
5347    EditControlPointSpline {
5348        points: ast::Expr,
5349        construction: Option<bool>,
5350    },
5351    EditConstraintValue {
5352        value: ast::BinaryPart,
5353    },
5354    EditDistanceConstraintLabelPosition {
5355        label_position: ast::Expr,
5356    },
5357    EditCallUnlabeled {
5358        arg: ast::Expr,
5359    },
5360    EditVarInitialValue {
5361        value: Number,
5362    },
5363    DeleteNode,
5364}
5365
5366impl AstMutateCommand {
5367    fn needs_defined_names_stack(&self) -> bool {
5368        matches!(
5369            self,
5370            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5371        )
5372    }
5373}
5374
5375#[derive(Debug)]
5376enum AstMutateCommandReturn {
5377    None,
5378    Name(String),
5379}
5380
5381#[derive(Debug, Clone)]
5382struct AstNodeRef {
5383    range: SourceRange,
5384    node_path: Option<ast::NodePath>,
5385}
5386
5387impl<T> From<&ast::Node<T>> for AstNodeRef {
5388    fn from(value: &ast::Node<T>) -> Self {
5389        AstNodeRef {
5390            range: value.into(),
5391            node_path: value.node_path.clone(),
5392        }
5393    }
5394}
5395
5396impl From<&ast::BodyItem> for AstNodeRef {
5397    fn from(value: &ast::BodyItem) -> Self {
5398        match value {
5399            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5400                range: node.into(),
5401                node_path: node.node_path.clone(),
5402            },
5403            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5404                range: node.into(),
5405                node_path: node.node_path.clone(),
5406            },
5407            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5408                range: node.into(),
5409                node_path: node.node_path.clone(),
5410            },
5411            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5412                range: node.into(),
5413                node_path: node.node_path.clone(),
5414            },
5415            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5416                range: node.into(),
5417                node_path: node.node_path.clone(),
5418            },
5419        }
5420    }
5421}
5422
5423impl From<&ast::Expr> for AstNodeRef {
5424    fn from(value: &ast::Expr) -> Self {
5425        AstNodeRef {
5426            range: SourceRange::from(value),
5427            node_path: value.node_path().cloned(),
5428        }
5429    }
5430}
5431
5432impl From<&AstMutateContext> for AstNodeRef {
5433    fn from(value: &AstMutateContext) -> Self {
5434        AstNodeRef {
5435            range: value.source_range,
5436            node_path: value.node_path.clone(),
5437        }
5438    }
5439}
5440
5441impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5442    type Error = crate::walk::AstNodeError;
5443
5444    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5445        Ok(AstNodeRef {
5446            range: SourceRange::try_from(value)?,
5447            node_path: value.try_into()?,
5448        })
5449    }
5450}
5451
5452impl From<AstNodeRef> for SourceRange {
5453    fn from(value: AstNodeRef) -> Self {
5454        value.range
5455    }
5456}
5457
5458impl Visitor for AstMutateContext {
5459    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5460    type Continue = ();
5461
5462    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5463        filter_and_process(self, node)
5464    }
5465
5466    fn finish(&mut self, node: NodeMut<'_>) {
5467        match &node {
5468            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5469                self.defined_names_stack.pop();
5470            }
5471            _ => {}
5472        }
5473    }
5474}
5475
5476fn filter_and_process(
5477    ctx: &mut AstMutateContext,
5478    node: NodeMut,
5479) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5480    let Ok(node_range) = SourceRange::try_from(&node) else {
5481        // Nodes that can't be converted to a range aren't interesting.
5482        return TraversalReturn::new_continue(());
5483    };
5484    // If we're adding a variable declaration, we need to look at variable
5485    // declaration expressions to see if it already has a variable, before
5486    // continuing. The variable declaration's source range won't match the
5487    // target; its init expression will.
5488    if let NodeMut::VariableDeclaration(var_decl) = &node {
5489        let expr_range = SourceRange::from(&var_decl.declaration.init);
5490        let expr_node_path = var_decl.declaration.init.node_path();
5491        if source_ref_matches(ctx, expr_range, expr_node_path) {
5492            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5493                // We found the variable declaration expression. It doesn't need
5494                // to be added.
5495                return TraversalReturn::new_break(Ok((
5496                    AstNodeRef::from(&**var_decl),
5497                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5498                )));
5499            }
5500            if let AstMutateCommand::DeleteNode = &ctx.command {
5501                // We found the variable declaration. Delete the variable along
5502                // with the segment.
5503                return TraversalReturn {
5504                    mutate_body_item: MutateBodyItem::Delete,
5505                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5506                };
5507            }
5508        }
5509    }
5510    // Similar thing with expression statement. We need to look at the
5511    // expression inside it.
5512    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5513        let expr_range = SourceRange::from(&expr_stmt.expression);
5514        let expr_node_path = expr_stmt.expression.node_path();
5515        if source_ref_matches(ctx, expr_range, expr_node_path) {
5516            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5517                // We found the node wrapped in an expression statement. Process
5518                // the statement.
5519                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5520                    return TraversalReturn::new_continue(());
5521                };
5522                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5523            }
5524            if let AstMutateCommand::DeleteNode = &ctx.command {
5525                // We found the node wrapped in an expression statement. Delete
5526                // the whole statement.
5527                return TraversalReturn {
5528                    mutate_body_item: MutateBodyItem::Delete,
5529                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5530                };
5531            }
5532        }
5533    }
5534
5535    if ctx.command.needs_defined_names_stack() {
5536        if let NodeMut::Program(program) = &node {
5537            ctx.defined_names_stack.push(find_defined_names(*program));
5538        } else if let NodeMut::SketchBlock(block) = &node {
5539            ctx.defined_names_stack.push(find_defined_names(&block.body));
5540        }
5541    }
5542
5543    // Make sure the node matches the source ref.
5544    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5545    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5546        return TraversalReturn::new_continue(());
5547    }
5548    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5549        return TraversalReturn::new_continue(());
5550    };
5551    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5552}
5553
5554fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5555    match &ctx.node_path {
5556        Some(target) => Some(target) == node_path,
5557        None => node_range == ctx.source_range,
5558    }
5559}
5560
5561fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5562    match &ctx.command {
5563        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5564            if let NodeMut::SketchBlock(sketch_block) = node {
5565                sketch_block
5566                    .body
5567                    .items
5568                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5569                        inner: ast::ExpressionStatement {
5570                            expression: expr.clone(),
5571                            digest: None,
5572                        },
5573                        start: Default::default(),
5574                        end: Default::default(),
5575                        module_id: Default::default(),
5576                        node_path: None,
5577                        outer_attrs: Default::default(),
5578                        pre_comments: Default::default(),
5579                        comment_start: Default::default(),
5580                    }));
5581                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5582            }
5583        }
5584        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5585            if let NodeMut::SketchBlock(sketch_block) = node {
5586                let empty_defined_names = HashSet::new();
5587                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5588                let Ok(name) = next_free_name(prefix, defined_names) else {
5589                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5590                };
5591                sketch_block
5592                    .body
5593                    .items
5594                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5595                        ast::VariableDeclaration::new(
5596                            ast::VariableDeclarator::new(&name, expr.clone()),
5597                            ast::ItemVisibility::Default,
5598                            ast::VariableKind::Const,
5599                        ),
5600                    ))));
5601                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5602            }
5603        }
5604        AstMutateCommand::AddVariableDeclaration { prefix } => {
5605            if let NodeMut::VariableDeclaration(inner) = node {
5606                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5607            }
5608            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5609                let empty_defined_names = HashSet::new();
5610                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5611                let Ok(name) = next_free_name(prefix, defined_names) else {
5612                    // TODO: Return an error instead?
5613                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5614                };
5615                let mutate_node =
5616                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5617                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5618                        ast::ItemVisibility::Default,
5619                        ast::VariableKind::Const,
5620                    ))));
5621                return TraversalReturn {
5622                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5623                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5624                };
5625            }
5626        }
5627        AstMutateCommand::EditPoint { at } => {
5628            if let NodeMut::CallExpressionKw(call) = node {
5629                if call.callee.name.name != POINT_FN {
5630                    return TraversalReturn::new_continue(());
5631                }
5632                // Update the arguments.
5633                for labeled_arg in &mut call.arguments {
5634                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5635                        labeled_arg.arg = at.clone();
5636                    }
5637                }
5638                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5639            }
5640        }
5641        AstMutateCommand::EditLine {
5642            start,
5643            end,
5644            construction,
5645        } => {
5646            if let NodeMut::CallExpressionKw(call) = node {
5647                if call.callee.name.name != LINE_FN {
5648                    return TraversalReturn::new_continue(());
5649                }
5650                // Update the arguments.
5651                for labeled_arg in &mut call.arguments {
5652                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5653                        labeled_arg.arg = start.clone();
5654                    }
5655                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5656                        labeled_arg.arg = end.clone();
5657                    }
5658                }
5659                // Handle construction kwarg
5660                if let Some(construction_value) = construction {
5661                    let construction_exists = call
5662                        .arguments
5663                        .iter()
5664                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5665                    if *construction_value {
5666                        // Add or update construction=true
5667                        if construction_exists {
5668                            // Update existing construction kwarg
5669                            for labeled_arg in &mut call.arguments {
5670                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5671                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5672                                        value: ast::LiteralValue::Bool(true),
5673                                        raw: "true".to_string(),
5674                                        digest: None,
5675                                    })));
5676                                }
5677                            }
5678                        } else {
5679                            // Add new construction kwarg
5680                            call.arguments.push(ast::LabeledArg {
5681                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5682                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5683                                    value: ast::LiteralValue::Bool(true),
5684                                    raw: "true".to_string(),
5685                                    digest: None,
5686                                }))),
5687                            });
5688                        }
5689                    } else {
5690                        // Remove construction kwarg if it exists
5691                        call.arguments
5692                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5693                    }
5694                }
5695                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5696            }
5697        }
5698        AstMutateCommand::EditArc {
5699            start,
5700            end,
5701            center,
5702            construction,
5703        } => {
5704            if let NodeMut::CallExpressionKw(call) = node {
5705                if call.callee.name.name != ARC_FN {
5706                    return TraversalReturn::new_continue(());
5707                }
5708                // Update the arguments.
5709                for labeled_arg in &mut call.arguments {
5710                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5711                        labeled_arg.arg = start.clone();
5712                    }
5713                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5714                        labeled_arg.arg = end.clone();
5715                    }
5716                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5717                        labeled_arg.arg = center.clone();
5718                    }
5719                }
5720                // Handle construction kwarg
5721                if let Some(construction_value) = construction {
5722                    let construction_exists = call
5723                        .arguments
5724                        .iter()
5725                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5726                    if *construction_value {
5727                        // Add or update construction=true
5728                        if construction_exists {
5729                            // Update existing construction kwarg
5730                            for labeled_arg in &mut call.arguments {
5731                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5732                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5733                                        value: ast::LiteralValue::Bool(true),
5734                                        raw: "true".to_string(),
5735                                        digest: None,
5736                                    })));
5737                                }
5738                            }
5739                        } else {
5740                            // Add new construction kwarg
5741                            call.arguments.push(ast::LabeledArg {
5742                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5743                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5744                                    value: ast::LiteralValue::Bool(true),
5745                                    raw: "true".to_string(),
5746                                    digest: None,
5747                                }))),
5748                            });
5749                        }
5750                    } else {
5751                        // Remove construction kwarg if it exists
5752                        call.arguments
5753                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5754                    }
5755                }
5756                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5757            }
5758        }
5759        AstMutateCommand::EditCircle {
5760            start,
5761            center,
5762            construction,
5763        } => {
5764            if let NodeMut::CallExpressionKw(call) = node {
5765                if call.callee.name.name != CIRCLE_FN {
5766                    return TraversalReturn::new_continue(());
5767                }
5768                // Update the arguments.
5769                for labeled_arg in &mut call.arguments {
5770                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5771                        labeled_arg.arg = start.clone();
5772                    }
5773                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5774                        labeled_arg.arg = center.clone();
5775                    }
5776                }
5777                // Handle construction kwarg
5778                if let Some(construction_value) = construction {
5779                    let construction_exists = call
5780                        .arguments
5781                        .iter()
5782                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5783                    if *construction_value {
5784                        if construction_exists {
5785                            // Update existing construction kwarg
5786                            for labeled_arg in &mut call.arguments {
5787                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5788                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5789                                        value: ast::LiteralValue::Bool(true),
5790                                        raw: "true".to_string(),
5791                                        digest: None,
5792                                    })));
5793                                }
5794                            }
5795                        } else {
5796                            // Add new construction kwarg
5797                            call.arguments.push(ast::LabeledArg {
5798                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5799                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5800                                    value: ast::LiteralValue::Bool(true),
5801                                    raw: "true".to_string(),
5802                                    digest: None,
5803                                }))),
5804                            });
5805                        }
5806                    } else {
5807                        // Remove construction kwarg if it exists
5808                        call.arguments
5809                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5810                    }
5811                }
5812                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5813            }
5814        }
5815        AstMutateCommand::EditControlPointSpline { points, construction } => {
5816            if let NodeMut::CallExpressionKw(call) = node {
5817                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
5818                    return TraversalReturn::new_continue(());
5819                }
5820                for labeled_arg in &mut call.arguments {
5821                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
5822                    {
5823                        labeled_arg.arg = points.clone();
5824                    }
5825                }
5826                // Handle construction kwarg
5827                if let Some(construction_value) = construction {
5828                    let construction_exists = call
5829                        .arguments
5830                        .iter()
5831                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5832                    if *construction_value {
5833                        if construction_exists {
5834                            for labeled_arg in &mut call.arguments {
5835                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5836                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5837                                        value: ast::LiteralValue::Bool(true),
5838                                        raw: "true".to_string(),
5839                                        digest: None,
5840                                    })));
5841                                }
5842                            }
5843                        } else {
5844                            call.arguments.push(ast::LabeledArg {
5845                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5846                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5847                                    value: ast::LiteralValue::Bool(true),
5848                                    raw: "true".to_string(),
5849                                    digest: None,
5850                                }))),
5851                            });
5852                        }
5853                    } else {
5854                        call.arguments
5855                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5856                    }
5857                }
5858                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5859            }
5860        }
5861        AstMutateCommand::EditConstraintValue { value } => {
5862            if let NodeMut::BinaryExpression(binary_expr) = node {
5863                let left_is_constraint = matches!(
5864                    &binary_expr.left,
5865                    ast::BinaryPart::CallExpressionKw(call)
5866                        if matches!(
5867                            call.callee.name.name.as_str(),
5868                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5869                        )
5870                );
5871                if left_is_constraint {
5872                    binary_expr.right = value.clone();
5873                } else {
5874                    binary_expr.left = value.clone();
5875                }
5876
5877                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5878            }
5879        }
5880        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5881            if let NodeMut::BinaryExpression(binary_expr) = node {
5882                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5883                    return TraversalReturn::new_continue(());
5884                };
5885                if !matches!(
5886                    call.callee.name.name.as_str(),
5887                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5888                ) {
5889                    return TraversalReturn::new_continue(());
5890                }
5891
5892                if let Some(label_arg) = call
5893                    .arguments
5894                    .iter_mut()
5895                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5896                {
5897                    label_arg.arg = label_position.clone();
5898                } else {
5899                    call.arguments.push(ast::LabeledArg {
5900                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5901                        arg: label_position.clone(),
5902                    });
5903                }
5904
5905                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5906            }
5907        }
5908        AstMutateCommand::EditCallUnlabeled { arg } => {
5909            if let NodeMut::CallExpressionKw(call) = node {
5910                call.unlabeled = Some(arg.clone());
5911                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5912            }
5913        }
5914        AstMutateCommand::EditVarInitialValue { value } => {
5915            if let NodeMut::NumericLiteral(numeric_literal) = node {
5916                // Update the initial value.
5917                let Ok(literal) = to_source_number(*value) else {
5918                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5919                        "Could not convert number to AST literal: {:?}",
5920                        *value
5921                    ))));
5922                };
5923                *numeric_literal = ast::Node::no_src(literal);
5924                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5925            }
5926        }
5927        AstMutateCommand::DeleteNode => {
5928            return TraversalReturn {
5929                mutate_body_item: MutateBodyItem::Delete,
5930                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5931            };
5932        }
5933    }
5934    TraversalReturn::new_continue(())
5935}
5936
5937struct FindSketchBlockSourceRange {
5938    /// The source range of the sketch block before mutation.
5939    target_before_mutation: SourceRange,
5940    /// The source range of the sketch block's last body item after mutation. We
5941    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5942    /// shared reference.
5943    found: Cell<Option<AstNodeRef>>,
5944}
5945
5946impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5947    type Error = crate::front::Error;
5948
5949    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5950        let Ok(node_range) = SourceRange::try_from(&node) else {
5951            return Ok(true);
5952        };
5953
5954        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5955            if node_range.module_id() == self.target_before_mutation.module_id()
5956                && node_range.start() == self.target_before_mutation.start()
5957                // End shouldn't match since we added something.
5958                && node_range.end() >= self.target_before_mutation.end()
5959            {
5960                self.found.set(sketch_block.body.items.last().map(|item| match item {
5961                    // For declarations like `circle1 = circle(...)`, use
5962                    // the init expression range so lookup in source_range_to_object
5963                    // matches the segment source range.
5964                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5965                    _ => AstNodeRef::from(item),
5966                }));
5967                return Ok(false);
5968            } else {
5969                // We found a different sketch block. No need to descend into
5970                // its children since sketch blocks cannot be nested.
5971                return Ok(true);
5972            }
5973        }
5974
5975        for child in node.children().iter() {
5976            if !child.visit(*self)? {
5977                return Ok(false);
5978            }
5979        }
5980
5981        Ok(true)
5982    }
5983}
5984
5985struct FindSketchBlockByNodePath {
5986    /// The Node Path of the sketch block before mutation.
5987    target_node_path: ast::NodePath,
5988    /// The ref of the sketch block's last body item after mutation. We need to
5989    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5990    /// reference.
5991    found: Cell<Option<AstNodeRef>>,
5992}
5993
5994impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5995    type Error = crate::front::Error;
5996
5997    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5998        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5999            return Ok(true);
6000        };
6001
6002        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6003            if let Some(node_path) = node_path
6004                && node_path == self.target_node_path
6005            {
6006                self.found.set(sketch_block.body.items.last().map(|item| match item {
6007                    // For declarations like `circle1 = circle(...)`, use
6008                    // the init expression range so lookup in source_range_to_object
6009                    // matches the segment source range.
6010                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6011                    _ => AstNodeRef::from(item),
6012                }));
6013
6014                return Ok(false);
6015            } else {
6016                // We found a different sketch block. No need to descend into
6017                // its children since sketch blocks cannot be nested.
6018                return Ok(true);
6019            }
6020        }
6021
6022        for child in node.children().iter() {
6023            if !child.visit(*self)? {
6024                return Ok(false);
6025            }
6026        }
6027
6028        Ok(true)
6029    }
6030}
6031
6032/// After adding an item to a sketch block, find the sketch block, and get the
6033/// source range of the added item. We assume that the added item is the last
6034/// item in the sketch block and that the sketch block's source range has grown,
6035/// but not moved from its starting offset.
6036///
6037/// TODO: Do we need to format *before* mutation in case formatting moves the
6038/// sketch block forward?
6039fn find_sketch_block_added_item(
6040    ast: &ast::Node<ast::Program>,
6041    sketch_block_before_mutation: &AstNodeRef,
6042) -> Result<AstNodeRef, KclError> {
6043    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6044        let find = FindSketchBlockByNodePath {
6045            target_node_path: node_path.clone(),
6046            found: Cell::new(None),
6047        };
6048        let node = crate::walk::Node::from(ast);
6049        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6050        find.found.into_inner().ok_or_else(|| {
6051            KclError::refactor(format!(
6052                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6053            ))
6054        })
6055    } else {
6056        // No NodePath. Fall back to legacy source range.
6057        let find = FindSketchBlockSourceRange {
6058            target_before_mutation: sketch_block_before_mutation.range,
6059            found: Cell::new(None),
6060        };
6061        let node = crate::walk::Node::from(ast);
6062        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6063        find.found.into_inner().ok_or_else(|| KclError::refactor(
6064            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?"),
6065        ))
6066    }
6067}
6068
6069fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6070    // TODO: Don't duplicate this from lib.rs Program.
6071    ast.recast_top(&Default::default(), 0)
6072}
6073
6074pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6075    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6076        inner: ast::ArrayExpression {
6077            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6078            non_code_meta: Default::default(),
6079            digest: None,
6080        },
6081        start: Default::default(),
6082        end: Default::default(),
6083        module_id: Default::default(),
6084        node_path: None,
6085        outer_attrs: Default::default(),
6086        pre_comments: Default::default(),
6087        comment_start: Default::default(),
6088    })))
6089}
6090
6091pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6092    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6093        ast::ArrayExpression {
6094            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6095            digest: None,
6096            non_code_meta: Default::default(),
6097        },
6098    ))))
6099}
6100
6101fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6102    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6103        ast::ArrayExpression {
6104            elements: vec![
6105                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6106                    point.x,
6107                )?)))),
6108                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6109                    point.y,
6110                )?)))),
6111            ],
6112            non_code_meta: Default::default(),
6113            digest: None,
6114        },
6115    ))))
6116}
6117
6118fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6119    match expr {
6120        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6121            inner: ast::Literal::from(to_source_number(*number)?),
6122            start: Default::default(),
6123            end: Default::default(),
6124            module_id: Default::default(),
6125            node_path: None,
6126            outer_attrs: Default::default(),
6127            pre_comments: Default::default(),
6128            comment_start: Default::default(),
6129        }))),
6130        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6131            inner: ast::SketchVar {
6132                initial: Some(Box::new(ast::Node {
6133                    inner: to_source_number(*number)?,
6134                    start: Default::default(),
6135                    end: Default::default(),
6136                    module_id: Default::default(),
6137                    node_path: None,
6138                    outer_attrs: Default::default(),
6139                    pre_comments: Default::default(),
6140                    comment_start: Default::default(),
6141                })),
6142                digest: None,
6143            },
6144            start: Default::default(),
6145            end: Default::default(),
6146            module_id: Default::default(),
6147            node_path: None,
6148            outer_attrs: Default::default(),
6149            pre_comments: Default::default(),
6150            comment_start: Default::default(),
6151        }))),
6152        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6153    }
6154}
6155
6156fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6157    Ok(ast::NumericLiteral {
6158        value: number.value,
6159        suffix: number.units,
6160        raw: format_number_literal(number.value, number.units, None)?,
6161        digest: None,
6162    })
6163}
6164
6165pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6166    ast::Expr::Name(Box::new(ast_name(name)))
6167}
6168
6169fn ast_name(name: String) -> ast::Node<ast::Name> {
6170    ast::Node {
6171        inner: ast::Name {
6172            name: ast::Node {
6173                inner: ast::Identifier { name, digest: None },
6174                start: Default::default(),
6175                end: Default::default(),
6176                module_id: Default::default(),
6177                node_path: None,
6178                outer_attrs: Default::default(),
6179                pre_comments: Default::default(),
6180                comment_start: Default::default(),
6181            },
6182            path: Vec::new(),
6183            abs_path: false,
6184            digest: None,
6185        },
6186        start: Default::default(),
6187        end: Default::default(),
6188        module_id: Default::default(),
6189        node_path: None,
6190        outer_attrs: Default::default(),
6191        pre_comments: Default::default(),
6192        comment_start: Default::default(),
6193    }
6194}
6195
6196pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6197    ast::Name {
6198        name: ast::Node {
6199            inner: ast::Identifier {
6200                name: name.to_owned(),
6201                digest: None,
6202            },
6203            start: Default::default(),
6204            end: Default::default(),
6205            module_id: Default::default(),
6206            node_path: None,
6207            outer_attrs: Default::default(),
6208            pre_comments: Default::default(),
6209            comment_start: Default::default(),
6210        },
6211        path: Default::default(),
6212        abs_path: false,
6213        digest: None,
6214    }
6215}
6216
6217// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6218
6219/// Create an AST node for coincident([expr1, expr2, ...])
6220pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6221    let elements = exprs.into_iter().collect::<Vec<_>>();
6222    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6223
6224    // Create array [expr1, expr2, ...]
6225    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6226        elements,
6227        digest: None,
6228        non_code_meta: Default::default(),
6229    })));
6230
6231    // Create coincident([...])
6232    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6233        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6234        unlabeled: Some(array_expr),
6235        arguments: Default::default(),
6236        digest: None,
6237        non_code_meta: Default::default(),
6238    })))
6239}
6240
6241/// Create an AST node for line(start = [...], end = [...])
6242pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6243    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6244        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6245        unlabeled: None,
6246        arguments: vec![
6247            ast::LabeledArg {
6248                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6249                arg: start_ast,
6250            },
6251            ast::LabeledArg {
6252                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6253                arg: end_ast,
6254            },
6255        ],
6256        digest: None,
6257        non_code_meta: Default::default(),
6258    })))
6259}
6260
6261/// Create an AST node for arc(start = [...], end = [...], center = [...])
6262pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6263    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6264        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6265        unlabeled: None,
6266        arguments: vec![
6267            ast::LabeledArg {
6268                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6269                arg: start_ast,
6270            },
6271            ast::LabeledArg {
6272                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6273                arg: end_ast,
6274            },
6275            ast::LabeledArg {
6276                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6277                arg: center_ast,
6278            },
6279        ],
6280        digest: None,
6281        non_code_meta: Default::default(),
6282    })))
6283}
6284
6285/// Create an AST node for circle(start = [...], center = [...])
6286pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6287    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6288        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6289        unlabeled: None,
6290        arguments: vec![
6291            ast::LabeledArg {
6292                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6293                arg: start_ast,
6294            },
6295            ast::LabeledArg {
6296                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6297                arg: center_ast,
6298            },
6299        ],
6300        digest: None,
6301        non_code_meta: Default::default(),
6302    })))
6303}
6304
6305/// Create an AST node for horizontal(line)
6306pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6307    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6308        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6309        unlabeled: Some(line_expr),
6310        arguments: Default::default(),
6311        digest: None,
6312        non_code_meta: Default::default(),
6313    })))
6314}
6315
6316/// Create an AST node for vertical(line)
6317pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6318    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6319        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6320        unlabeled: Some(line_expr),
6321        arguments: Default::default(),
6322        digest: None,
6323        non_code_meta: Default::default(),
6324    })))
6325}
6326
6327/// Create a member expression like object.property (e.g., line1.end)
6328pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6329    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6330        object: object_expr,
6331        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6332            name: ast::Node::no_src(ast::Identifier {
6333                name: property.to_string(),
6334                digest: None,
6335            }),
6336            path: Vec::new(),
6337            abs_path: false,
6338            digest: None,
6339        }))),
6340        computed: false,
6341        digest: None,
6342    })))
6343}
6344
6345pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6346    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6347        object: object_expr,
6348        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6349            value: index as f64,
6350            suffix: NumericSuffix::None,
6351            raw: index.to_string(),
6352            digest: None,
6353        })))),
6354        computed: true,
6355        digest: None,
6356    })))
6357}
6358
6359/// Create an AST node for `fixed([point, [x, y]])`.
6360fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6361    // Create [x, y] array literal.
6362    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6363        position.x,
6364    )?))));
6365    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6366        position.y,
6367    )?))));
6368    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6369        elements: vec![x_literal, y_literal],
6370        digest: None,
6371        non_code_meta: Default::default(),
6372    })));
6373
6374    // Create [point, [x, y]] outer array.
6375    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6376        elements: vec![point_expr, point_array],
6377        digest: None,
6378        non_code_meta: Default::default(),
6379    })));
6380
6381    // Create fixed([...])
6382    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6383        ast::CallExpressionKw {
6384            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6385            unlabeled: Some(array_expr),
6386            arguments: Default::default(),
6387            digest: None,
6388            non_code_meta: Default::default(),
6389        },
6390    ))))
6391}
6392
6393/// Create an AST node for equalLength([line1, line2, ...])
6394pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6395    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6396        elements: line_exprs,
6397        digest: None,
6398        non_code_meta: Default::default(),
6399    })));
6400
6401    // Create equalLength([...])
6402    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6403        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6404        unlabeled: Some(array_expr),
6405        arguments: Default::default(),
6406        digest: None,
6407        non_code_meta: Default::default(),
6408    })))
6409}
6410
6411/// Create an AST node for equalRadius([seg1, seg2, ...])
6412pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6413    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6414        elements: segment_exprs,
6415        digest: None,
6416        non_code_meta: Default::default(),
6417    })));
6418
6419    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6420        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6421        unlabeled: Some(array_expr),
6422        arguments: Default::default(),
6423        digest: None,
6424        non_code_meta: Default::default(),
6425    })))
6426}
6427
6428/// Create an AST node for tangent([seg1, seg2])
6429pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6430    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6431        elements: vec![seg1_expr, seg2_expr],
6432        digest: None,
6433        non_code_meta: Default::default(),
6434    })));
6435
6436    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6437        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6438        unlabeled: Some(array_expr),
6439        arguments: Default::default(),
6440        digest: None,
6441        non_code_meta: Default::default(),
6442    })))
6443}
6444
6445/// Create an AST node for symmetric([input1, input2], axis = line)
6446pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6447    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6448        elements: input_exprs,
6449        digest: None,
6450        non_code_meta: Default::default(),
6451    })));
6452    let arguments = vec![ast::LabeledArg {
6453        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6454        arg: axis_expr,
6455    }];
6456
6457    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6458        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6459        unlabeled: Some(array_expr),
6460        arguments,
6461        digest: None,
6462        non_code_meta: Default::default(),
6463    })))
6464}
6465
6466/// Create an AST node for midpoint(segment, point = point)
6467pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6468    let arguments = vec![ast::LabeledArg {
6469        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6470        arg: point_expr,
6471    }];
6472
6473    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6474        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6475        unlabeled: Some(segment_expr),
6476        arguments,
6477        digest: None,
6478        non_code_meta: Default::default(),
6479    })))
6480}
6481
6482#[cfg(test)]
6483mod tests {
6484    use super::*;
6485    use crate::engine::PlaneName;
6486    use crate::execution::cache::SketchModeState;
6487    use crate::execution::cache::clear_mem_cache;
6488    use crate::execution::cache::read_old_memory;
6489    use crate::execution::cache::write_old_memory;
6490    use crate::front::Distance;
6491    use crate::front::Fixed;
6492    use crate::front::FixedPoint;
6493    use crate::front::Midpoint;
6494    use crate::front::Object;
6495    use crate::front::Plane;
6496    use crate::front::Sketch;
6497    use crate::front::Tangent;
6498    use crate::frontend::sketch::Vertical;
6499    use crate::pretty::NumericSuffix;
6500
6501    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6502        for object in &scene_graph.objects {
6503            if let ObjectKind::Sketch(_) = &object.kind {
6504                return Some(object);
6505            }
6506        }
6507        None
6508    }
6509
6510    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6511        for object in &scene_graph.objects {
6512            if let ObjectKind::Face(_) = &object.kind {
6513                return Some(object);
6514            }
6515        }
6516        None
6517    }
6518
6519    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6520        for object in &scene_graph.objects {
6521            if matches!(&object.kind, ObjectKind::Wall(_)) {
6522                return Some(object.id);
6523            }
6524        }
6525        None
6526    }
6527
6528    #[test]
6529    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6530        let source = "\
6531region001 = region(point = [0.1, 0.1], sketch = s)
6532extrude001 = extrude(region001, length = 5)
6533revolve001 = revolve(region001, axis = Y)
6534sweep001 = sweep(region001, path = path001)
6535loft001 = loft(region001)
6536not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6537";
6538
6539        let program = Program::parse(source).unwrap().0.unwrap();
6540
6541        assert_eq!(
6542            region_name_from_sweep_variable(&program.ast, "extrude001"),
6543            Some("region001".to_owned())
6544        );
6545        assert_eq!(
6546            region_name_from_sweep_variable(&program.ast, "revolve001"),
6547            Some("region001".to_owned())
6548        );
6549        assert_eq!(
6550            region_name_from_sweep_variable(&program.ast, "sweep001"),
6551            Some("region001".to_owned())
6552        );
6553        assert_eq!(
6554            region_name_from_sweep_variable(&program.ast, "loft001"),
6555            Some("region001".to_owned())
6556        );
6557        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6558    }
6559
6560    #[track_caller]
6561    fn expect_sketch(object: &Object) -> &Sketch {
6562        if let ObjectKind::Sketch(sketch) = &object.kind {
6563            sketch
6564        } else {
6565            panic!("Object is not a sketch: {:?}", object);
6566        }
6567    }
6568
6569    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6570        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6571        let ObjectKind::Segment {
6572            segment: Segment::Point(point),
6573        } = &point_object.kind
6574        else {
6575            panic!("Object is not a point segment: {point_object:?}");
6576        };
6577        point.position.clone()
6578    }
6579
6580    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6581        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6582        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6583    }
6584
6585    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6586        LineCtor {
6587            start: Point2d {
6588                x: Expr::Number(Number { value: start_x, units }),
6589                y: Expr::Number(Number { value: start_y, units }),
6590            },
6591            end: Point2d {
6592                x: Expr::Number(Number { value: end_x, units }),
6593                y: Expr::Number(Number { value: end_y, units }),
6594            },
6595            construction: None,
6596        }
6597    }
6598
6599    async fn create_sketch_with_single_line(
6600        frontend: &mut FrontendState,
6601        ctx: &ExecutorContext,
6602        mock_ctx: &ExecutorContext,
6603        version: Version,
6604    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6605        frontend.program = Program::empty();
6606
6607        let sketch_args = SketchCtor {
6608            on: Plane::Default(PlaneName::Xy),
6609        };
6610        let (_src_delta, _scene_delta, sketch_id) = frontend
6611            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6612            .await
6613            .unwrap();
6614
6615        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6616        let (source_delta, scene_graph_delta) = frontend
6617            .add_segment(mock_ctx, version, sketch_id, segment, None)
6618            .await
6619            .unwrap();
6620        let line_id = *scene_graph_delta
6621            .new_objects
6622            .last()
6623            .expect("Expected line object id to be created");
6624
6625        (sketch_id, line_id, source_delta, scene_graph_delta)
6626    }
6627
6628    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
6629        frontend.program = program.clone();
6630        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
6631        frontend.update_state_after_exec(outcome, true);
6632    }
6633
6634    #[tokio::test(flavor = "multi_thread")]
6635    async fn test_sketch_checkpoint_round_trip_restores_state() {
6636        let mut frontend = FrontendState::new();
6637        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6638        let mock_ctx = ExecutorContext::new_mock(None).await;
6639        let version = Version(0);
6640
6641        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6642            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6643
6644        let expected_source = source_delta.text.clone();
6645        let expected_scene_graph = frontend.scene_graph.clone();
6646        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6647        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6648
6649        let checkpoint_id = frontend
6650            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6651            .await
6652            .unwrap();
6653
6654        let edited_segments = vec![ExistingSegmentCtor {
6655            id: line_id,
6656            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6657        }];
6658        let (edited_source, _edited_scene) = frontend
6659            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6660            .await
6661            .unwrap();
6662        assert_ne!(edited_source.text, expected_source);
6663
6664        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6665
6666        assert_eq!(restored.source_delta.text, expected_source);
6667        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6668        assert!(restored.scene_graph_delta.invalidates_ids);
6669        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6670        assert_eq!(frontend.scene_graph, expected_scene_graph);
6671        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6672
6673        ctx.close().await;
6674    }
6675
6676    #[tokio::test(flavor = "multi_thread")]
6677    async fn test_sketch_checkpoints_prune_oldest_entries() {
6678        let mut frontend = FrontendState::new();
6679        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6680        let mock_ctx = ExecutorContext::new_mock(None).await;
6681        let version = Version(0);
6682
6683        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6684            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6685
6686        let mut checkpoint_ids = Vec::new();
6687        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6688            checkpoint_ids.push(
6689                frontend
6690                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6691                    .await
6692                    .unwrap(),
6693            );
6694        }
6695
6696        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6697        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6698
6699        let oldest_retained = checkpoint_ids[3];
6700        assert_eq!(
6701            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6702            Some(oldest_retained)
6703        );
6704
6705        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6706        assert!(evicted_restore.is_err());
6707        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6708
6709        frontend
6710            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6711            .await
6712            .unwrap();
6713
6714        ctx.close().await;
6715    }
6716
6717    #[tokio::test(flavor = "multi_thread")]
6718    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6719        let mut frontend = FrontendState::new();
6720        let missing_checkpoint = SketchCheckpointId::new(999);
6721
6722        let err = frontend
6723            .restore_sketch_checkpoint(missing_checkpoint)
6724            .await
6725            .expect_err("Expected restore to fail for missing checkpoint");
6726
6727        assert!(err.msg.contains("Sketch checkpoint not found"));
6728    }
6729
6730    #[tokio::test(flavor = "multi_thread")]
6731    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6732        let mut frontend = FrontendState::new();
6733        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6734        let mock_ctx = ExecutorContext::new_mock(None).await;
6735        let version = Version(0);
6736
6737        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6738            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6739
6740        let checkpoint_a = frontend
6741            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6742            .await
6743            .unwrap();
6744        let checkpoint_b = frontend
6745            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6746            .await
6747            .unwrap();
6748        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6749
6750        frontend.clear_sketch_checkpoints();
6751        assert!(frontend.sketch_checkpoints.is_empty());
6752        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6753        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6754
6755        ctx.close().await;
6756    }
6757
6758    #[tokio::test(flavor = "multi_thread")]
6759    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6760        let mut frontend = FrontendState::new();
6761        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6762        let mock_ctx = ExecutorContext::new_mock(None).await;
6763        let version = Version(0);
6764
6765        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6766            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6767        let old_source = source_delta.text.clone();
6768        let old_checkpoint = frontend
6769            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6770            .await
6771            .unwrap();
6772        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6773
6774        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6775            .unwrap()
6776            .0
6777            .unwrap();
6778
6779        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6780        let SetProgramOutcome::Success {
6781            checkpoint_id: Some(new_checkpoint),
6782            ..
6783        } = result
6784        else {
6785            panic!("Expected Success with a fresh checkpoint baseline");
6786        };
6787
6788        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6789
6790        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6791        assert_eq!(old_restore.source_delta.text, old_source);
6792
6793        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6794        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6795
6796        ctx.close().await;
6797    }
6798
6799    #[tokio::test(flavor = "multi_thread")]
6800    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6801        let mut frontend = FrontendState::new();
6802        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6803        let mock_ctx = ExecutorContext::new_mock(None).await;
6804        let version = Version(0);
6805
6806        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6807            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6808        let old_checkpoint = frontend
6809            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6810            .await
6811            .unwrap();
6812        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6813
6814        let failing_program = Program::parse(
6815            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6816        )
6817        .unwrap()
6818        .0
6819        .unwrap();
6820
6821        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6822        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6823        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6824        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6825
6826        ctx.close().await;
6827    }
6828
6829    #[tokio::test(flavor = "multi_thread")]
6830    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6831        let mut frontend = FrontendState::new();
6832        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6833
6834        let program = Program::parse(
6835            "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",
6836        )
6837        .unwrap()
6838        .0
6839        .unwrap();
6840        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6841        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6842            panic!("Expected successful baseline program execution");
6843        };
6844
6845        clear_mem_cache().await;
6846        assert!(read_old_memory().await.is_none());
6847
6848        let checkpoint_without_mock_memory = frontend
6849            .create_sketch_checkpoint((*exec_outcome).clone())
6850            .await
6851            .unwrap();
6852
6853        write_old_memory(SketchModeState::new_for_tests()).await;
6854        assert!(read_old_memory().await.is_some());
6855
6856        let checkpoint_with_mock_memory = frontend
6857            .create_sketch_checkpoint((*exec_outcome).clone())
6858            .await
6859            .unwrap();
6860
6861        clear_mem_cache().await;
6862        assert!(read_old_memory().await.is_none());
6863
6864        frontend
6865            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6866            .await
6867            .unwrap();
6868        assert!(read_old_memory().await.is_some());
6869
6870        frontend
6871            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6872            .await
6873            .unwrap();
6874        assert!(read_old_memory().await.is_none());
6875
6876        ctx.close().await;
6877    }
6878
6879    #[tokio::test(flavor = "multi_thread")]
6880    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6881        let source = "\
6882sketch(on = XY) {
6883  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6884}
6885
6886bad = missing_name
6887";
6888        let program = Program::parse(source).unwrap().0.unwrap();
6889
6890        let mut frontend = FrontendState::new();
6891
6892        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6893        let mock_ctx = ExecutorContext::new_mock(None).await;
6894        let version = Version(0);
6895        let project_id = ProjectId(0);
6896        let file_id = FileId(0);
6897
6898        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6899            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6900        };
6901
6902        let sketch_id = frontend
6903            .scene_graph
6904            .objects
6905            .iter()
6906            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6907            .expect("Expected sketch object from errored hack_set_program");
6908
6909        frontend
6910            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6911            .await
6912            .unwrap();
6913
6914        ctx.close().await;
6915        mock_ctx.close().await;
6916    }
6917
6918    #[tokio::test(flavor = "multi_thread")]
6919    async fn test_new_sketch_add_point_edit_point() {
6920        let program = Program::empty();
6921
6922        let mut frontend = FrontendState::new();
6923        frontend.program = program;
6924
6925        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6926        let mock_ctx = ExecutorContext::new_mock(None).await;
6927        let version = Version(0);
6928
6929        let sketch_args = SketchCtor {
6930            on: Plane::Default(PlaneName::Xy),
6931        };
6932        let (_src_delta, scene_delta, sketch_id) = frontend
6933            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6934            .await
6935            .unwrap();
6936        assert_eq!(sketch_id, ObjectId(1));
6937        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6938        let sketch_object = &scene_delta.new_graph.objects[1];
6939        assert_eq!(sketch_object.id, ObjectId(1));
6940        assert_eq!(
6941            sketch_object.kind,
6942            ObjectKind::Sketch(Sketch {
6943                args: SketchCtor {
6944                    on: Plane::Default(PlaneName::Xy)
6945                },
6946                plane: ObjectId(0),
6947                segments: vec![],
6948                constraints: vec![],
6949            })
6950        );
6951        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6952
6953        let point_ctor = PointCtor {
6954            position: Point2d {
6955                x: Expr::Number(Number {
6956                    value: 1.0,
6957                    units: NumericSuffix::Inch,
6958                }),
6959                y: Expr::Number(Number {
6960                    value: 2.0,
6961                    units: NumericSuffix::Inch,
6962                }),
6963            },
6964        };
6965        let segment = SegmentCtor::Point(point_ctor);
6966        let (src_delta, scene_delta) = frontend
6967            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6968            .await
6969            .unwrap();
6970        assert_eq!(
6971            src_delta.text.as_str(),
6972            "sketch001 = sketch(on = XY) {
6973  point(at = [1in, 2in])
6974}
6975"
6976        );
6977        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6978        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6979        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6980            assert_eq!(scene_object.id.0, i);
6981        }
6982
6983        let point_id = *scene_delta.new_objects.last().unwrap();
6984
6985        let point_ctor = PointCtor {
6986            position: Point2d {
6987                x: Expr::Number(Number {
6988                    value: 3.0,
6989                    units: NumericSuffix::Inch,
6990                }),
6991                y: Expr::Number(Number {
6992                    value: 4.0,
6993                    units: NumericSuffix::Inch,
6994                }),
6995            },
6996        };
6997        let segments = vec![ExistingSegmentCtor {
6998            id: point_id,
6999            ctor: SegmentCtor::Point(point_ctor),
7000        }];
7001        let (src_delta, scene_delta) = frontend
7002            .edit_segments(&mock_ctx, version, sketch_id, segments)
7003            .await
7004            .unwrap();
7005        assert_eq!(
7006            src_delta.text.as_str(),
7007            "sketch001 = sketch(on = XY) {
7008  point(at = [3in, 4in])
7009}
7010"
7011        );
7012        assert_eq!(scene_delta.new_objects, vec![]);
7013        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7014
7015        ctx.close().await;
7016        mock_ctx.close().await;
7017    }
7018
7019    #[tokio::test(flavor = "multi_thread")]
7020    async fn test_new_sketch_add_line_edit_line() {
7021        let program = Program::empty();
7022
7023        let mut frontend = FrontendState::new();
7024        frontend.program = program;
7025
7026        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7027        let mock_ctx = ExecutorContext::new_mock(None).await;
7028        let version = Version(0);
7029
7030        let sketch_args = SketchCtor {
7031            on: Plane::Default(PlaneName::Xy),
7032        };
7033        let (_src_delta, scene_delta, sketch_id) = frontend
7034            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7035            .await
7036            .unwrap();
7037        assert_eq!(sketch_id, ObjectId(1));
7038        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7039        let sketch_object = &scene_delta.new_graph.objects[1];
7040        assert_eq!(sketch_object.id, ObjectId(1));
7041        assert_eq!(
7042            sketch_object.kind,
7043            ObjectKind::Sketch(Sketch {
7044                args: SketchCtor {
7045                    on: Plane::Default(PlaneName::Xy)
7046                },
7047                plane: ObjectId(0),
7048                segments: vec![],
7049                constraints: vec![],
7050            })
7051        );
7052        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7053
7054        let line_ctor = LineCtor {
7055            start: Point2d {
7056                x: Expr::Number(Number {
7057                    value: 0.0,
7058                    units: NumericSuffix::Mm,
7059                }),
7060                y: Expr::Number(Number {
7061                    value: 0.0,
7062                    units: NumericSuffix::Mm,
7063                }),
7064            },
7065            end: Point2d {
7066                x: Expr::Number(Number {
7067                    value: 10.0,
7068                    units: NumericSuffix::Mm,
7069                }),
7070                y: Expr::Number(Number {
7071                    value: 10.0,
7072                    units: NumericSuffix::Mm,
7073                }),
7074            },
7075            construction: None,
7076        };
7077        let segment = SegmentCtor::Line(line_ctor);
7078        let (src_delta, scene_delta) = frontend
7079            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7080            .await
7081            .unwrap();
7082        assert_eq!(
7083            src_delta.text.as_str(),
7084            "sketch001 = sketch(on = XY) {
7085  line(start = [0mm, 0mm], end = [10mm, 10mm])
7086}
7087"
7088        );
7089        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7090        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7091        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7092            assert_eq!(scene_object.id.0, i);
7093        }
7094
7095        // The new objects are the end points and then the line.
7096        let line = *scene_delta.new_objects.last().unwrap();
7097
7098        let line_ctor = LineCtor {
7099            start: Point2d {
7100                x: Expr::Number(Number {
7101                    value: 1.0,
7102                    units: NumericSuffix::Mm,
7103                }),
7104                y: Expr::Number(Number {
7105                    value: 2.0,
7106                    units: NumericSuffix::Mm,
7107                }),
7108            },
7109            end: Point2d {
7110                x: Expr::Number(Number {
7111                    value: 13.0,
7112                    units: NumericSuffix::Mm,
7113                }),
7114                y: Expr::Number(Number {
7115                    value: 14.0,
7116                    units: NumericSuffix::Mm,
7117                }),
7118            },
7119            construction: None,
7120        };
7121        let segments = vec![ExistingSegmentCtor {
7122            id: line,
7123            ctor: SegmentCtor::Line(line_ctor),
7124        }];
7125        let (src_delta, scene_delta) = frontend
7126            .edit_segments(&mock_ctx, version, sketch_id, segments)
7127            .await
7128            .unwrap();
7129        assert_eq!(
7130            src_delta.text.as_str(),
7131            "sketch001 = sketch(on = XY) {
7132  line(start = [1mm, 2mm], end = [13mm, 14mm])
7133}
7134"
7135        );
7136        assert_eq!(scene_delta.new_objects, vec![]);
7137        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7138
7139        ctx.close().await;
7140        mock_ctx.close().await;
7141    }
7142
7143    #[tokio::test(flavor = "multi_thread")]
7144    async fn test_new_sketch_add_arc_edit_arc() {
7145        let program = Program::empty();
7146
7147        let mut frontend = FrontendState::new();
7148        frontend.program = program;
7149
7150        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7151        let mock_ctx = ExecutorContext::new_mock(None).await;
7152        let version = Version(0);
7153
7154        let sketch_args = SketchCtor {
7155            on: Plane::Default(PlaneName::Xy),
7156        };
7157        let (_src_delta, scene_delta, sketch_id) = frontend
7158            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7159            .await
7160            .unwrap();
7161        assert_eq!(sketch_id, ObjectId(1));
7162        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7163        let sketch_object = &scene_delta.new_graph.objects[1];
7164        assert_eq!(sketch_object.id, ObjectId(1));
7165        assert_eq!(
7166            sketch_object.kind,
7167            ObjectKind::Sketch(Sketch {
7168                args: SketchCtor {
7169                    on: Plane::Default(PlaneName::Xy),
7170                },
7171                plane: ObjectId(0),
7172                segments: vec![],
7173                constraints: vec![],
7174            })
7175        );
7176        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7177
7178        let arc_ctor = ArcCtor {
7179            start: Point2d {
7180                x: Expr::Var(Number {
7181                    value: 0.0,
7182                    units: NumericSuffix::Mm,
7183                }),
7184                y: Expr::Var(Number {
7185                    value: 0.0,
7186                    units: NumericSuffix::Mm,
7187                }),
7188            },
7189            end: Point2d {
7190                x: Expr::Var(Number {
7191                    value: 10.0,
7192                    units: NumericSuffix::Mm,
7193                }),
7194                y: Expr::Var(Number {
7195                    value: 10.0,
7196                    units: NumericSuffix::Mm,
7197                }),
7198            },
7199            center: Point2d {
7200                x: Expr::Var(Number {
7201                    value: 10.0,
7202                    units: NumericSuffix::Mm,
7203                }),
7204                y: Expr::Var(Number {
7205                    value: 0.0,
7206                    units: NumericSuffix::Mm,
7207                }),
7208            },
7209            construction: None,
7210        };
7211        let segment = SegmentCtor::Arc(arc_ctor);
7212        let (src_delta, scene_delta) = frontend
7213            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7214            .await
7215            .unwrap();
7216        assert_eq!(
7217            src_delta.text.as_str(),
7218            "sketch001 = sketch(on = XY) {
7219  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7220}
7221"
7222        );
7223        assert_eq!(
7224            scene_delta.new_objects,
7225            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7226        );
7227        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7228            assert_eq!(scene_object.id.0, i);
7229        }
7230        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7231
7232        // The new objects are the end points, the center, and then the arc.
7233        let arc = *scene_delta.new_objects.last().unwrap();
7234
7235        let arc_ctor = ArcCtor {
7236            start: Point2d {
7237                x: Expr::Var(Number {
7238                    value: 1.0,
7239                    units: NumericSuffix::Mm,
7240                }),
7241                y: Expr::Var(Number {
7242                    value: 2.0,
7243                    units: NumericSuffix::Mm,
7244                }),
7245            },
7246            end: Point2d {
7247                x: Expr::Var(Number {
7248                    value: 13.0,
7249                    units: NumericSuffix::Mm,
7250                }),
7251                y: Expr::Var(Number {
7252                    value: 14.0,
7253                    units: NumericSuffix::Mm,
7254                }),
7255            },
7256            center: Point2d {
7257                x: Expr::Var(Number {
7258                    value: 13.0,
7259                    units: NumericSuffix::Mm,
7260                }),
7261                y: Expr::Var(Number {
7262                    value: 2.0,
7263                    units: NumericSuffix::Mm,
7264                }),
7265            },
7266            construction: None,
7267        };
7268        let segments = vec![ExistingSegmentCtor {
7269            id: arc,
7270            ctor: SegmentCtor::Arc(arc_ctor),
7271        }];
7272        let (src_delta, scene_delta) = frontend
7273            .edit_segments(&mock_ctx, version, sketch_id, segments)
7274            .await
7275            .unwrap();
7276        assert_eq!(
7277            src_delta.text.as_str(),
7278            "sketch001 = sketch(on = XY) {
7279  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7280}
7281"
7282        );
7283        assert_eq!(scene_delta.new_objects, vec![]);
7284        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7285
7286        ctx.close().await;
7287        mock_ctx.close().await;
7288    }
7289
7290    #[tokio::test(flavor = "multi_thread")]
7291    async fn test_new_sketch_add_circle_edit_circle() {
7292        let program = Program::empty();
7293
7294        let mut frontend = FrontendState::new();
7295        frontend.program = program;
7296
7297        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7298        let mock_ctx = ExecutorContext::new_mock(None).await;
7299        let version = Version(0);
7300
7301        let sketch_args = SketchCtor {
7302            on: Plane::Default(PlaneName::Xy),
7303        };
7304        let (_src_delta, _scene_delta, sketch_id) = frontend
7305            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7306            .await
7307            .unwrap();
7308
7309        // Add a circle segment.
7310        let circle_ctor = CircleCtor {
7311            start: Point2d {
7312                x: Expr::Var(Number {
7313                    value: 5.0,
7314                    units: NumericSuffix::Mm,
7315                }),
7316                y: Expr::Var(Number {
7317                    value: 0.0,
7318                    units: NumericSuffix::Mm,
7319                }),
7320            },
7321            center: Point2d {
7322                x: Expr::Var(Number {
7323                    value: 0.0,
7324                    units: NumericSuffix::Mm,
7325                }),
7326                y: Expr::Var(Number {
7327                    value: 0.0,
7328                    units: NumericSuffix::Mm,
7329                }),
7330            },
7331            construction: None,
7332        };
7333        let segment = SegmentCtor::Circle(circle_ctor);
7334        let (src_delta, scene_delta) = frontend
7335            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7336            .await
7337            .unwrap();
7338        assert_eq!(
7339            src_delta.text.as_str(),
7340            "sketch001 = sketch(on = XY) {
7341  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7342}
7343"
7344        );
7345        // The new objects are start, center, and then the circle segment.
7346        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7347        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7348
7349        let circle = *scene_delta.new_objects.last().unwrap();
7350
7351        // Edit the circle segment.
7352        let circle_ctor = CircleCtor {
7353            start: Point2d {
7354                x: Expr::Var(Number {
7355                    value: 10.0,
7356                    units: NumericSuffix::Mm,
7357                }),
7358                y: Expr::Var(Number {
7359                    value: 0.0,
7360                    units: NumericSuffix::Mm,
7361                }),
7362            },
7363            center: Point2d {
7364                x: Expr::Var(Number {
7365                    value: 3.0,
7366                    units: NumericSuffix::Mm,
7367                }),
7368                y: Expr::Var(Number {
7369                    value: 4.0,
7370                    units: NumericSuffix::Mm,
7371                }),
7372            },
7373            construction: None,
7374        };
7375        let segments = vec![ExistingSegmentCtor {
7376            id: circle,
7377            ctor: SegmentCtor::Circle(circle_ctor),
7378        }];
7379        let (src_delta, scene_delta) = frontend
7380            .edit_segments(&mock_ctx, version, sketch_id, segments)
7381            .await
7382            .unwrap();
7383        assert_eq!(
7384            src_delta.text.as_str(),
7385            "sketch001 = sketch(on = XY) {
7386  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7387}
7388"
7389        );
7390        assert_eq!(scene_delta.new_objects, vec![]);
7391        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7392
7393        ctx.close().await;
7394        mock_ctx.close().await;
7395    }
7396
7397    #[tokio::test(flavor = "multi_thread")]
7398    async fn test_delete_circle() {
7399        let initial_source = "sketch001 = sketch(on = XY) {
7400  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7401}
7402";
7403
7404        let program = Program::parse(initial_source).unwrap().0.unwrap();
7405        let mut frontend = FrontendState::new();
7406
7407        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7408        let mock_ctx = ExecutorContext::new_mock(None).await;
7409        let version = Version(0);
7410
7411        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7412        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7413        let sketch_id = sketch_object.id;
7414        let sketch = expect_sketch(sketch_object);
7415
7416        // The sketch should have 3 segments: start point, center point, and the circle.
7417        assert_eq!(sketch.segments.len(), 3);
7418        let circle_id = sketch.segments[2];
7419
7420        // Delete the circle.
7421        let (src_delta, scene_delta) = frontend
7422            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7423            .await
7424            .unwrap();
7425        assert_eq!(
7426            src_delta.text.as_str(),
7427            "sketch001 = sketch(on = XY) {
7428}
7429"
7430        );
7431        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7432        let new_sketch = expect_sketch(new_sketch_object);
7433        assert_eq!(new_sketch.segments.len(), 0);
7434
7435        ctx.close().await;
7436        mock_ctx.close().await;
7437    }
7438
7439    #[tokio::test(flavor = "multi_thread")]
7440    async fn test_edit_circle_via_point() {
7441        let initial_source = "sketch001 = sketch(on = XY) {
7442  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7443}
7444";
7445
7446        let program = Program::parse(initial_source).unwrap().0.unwrap();
7447        let mut frontend = FrontendState::new();
7448
7449        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7450        let mock_ctx = ExecutorContext::new_mock(None).await;
7451        let version = Version(0);
7452
7453        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7454        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7455        let sketch_id = sketch_object.id;
7456        let sketch = expect_sketch(sketch_object);
7457
7458        // Find the circle segment and its start point.
7459        let circle_id = sketch
7460            .segments
7461            .iter()
7462            .copied()
7463            .find(|seg_id| {
7464                matches!(
7465                    &frontend.scene_graph.objects[seg_id.0].kind,
7466                    ObjectKind::Segment {
7467                        segment: Segment::Circle(_)
7468                    }
7469                )
7470            })
7471            .expect("Expected a circle segment in sketch");
7472        let circle_object = &frontend.scene_graph.objects[circle_id.0];
7473        let ObjectKind::Segment {
7474            segment: Segment::Circle(circle),
7475        } = &circle_object.kind
7476        else {
7477            panic!("Expected circle segment, got: {:?}", circle_object.kind);
7478        };
7479        let start_point_id = circle.start;
7480
7481        // Edit the start point via SegmentCtor::Point.
7482        let segments = vec![ExistingSegmentCtor {
7483            id: start_point_id,
7484            ctor: SegmentCtor::Point(PointCtor {
7485                position: Point2d {
7486                    x: Expr::Var(Number {
7487                        value: 7.0,
7488                        units: NumericSuffix::Mm,
7489                    }),
7490                    y: Expr::Var(Number {
7491                        value: 1.0,
7492                        units: NumericSuffix::Mm,
7493                    }),
7494                },
7495            }),
7496        }];
7497        let (src_delta, _scene_delta) = frontend
7498            .edit_segments(&mock_ctx, version, sketch_id, segments)
7499            .await
7500            .unwrap();
7501        assert_eq!(
7502            src_delta.text.as_str(),
7503            "sketch001 = sketch(on = XY) {
7504  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7505}
7506"
7507        );
7508
7509        ctx.close().await;
7510        mock_ctx.close().await;
7511    }
7512
7513    #[tokio::test(flavor = "multi_thread")]
7514    async fn test_add_line_when_sketch_block_uses_variable() {
7515        let initial_source = "s = sketch(on = XY) {}
7516";
7517
7518        let program = Program::parse(initial_source).unwrap().0.unwrap();
7519
7520        let mut frontend = FrontendState::new();
7521
7522        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7523        let mock_ctx = ExecutorContext::new_mock(None).await;
7524        let version = Version(0);
7525
7526        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7527        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7528        let sketch_id = sketch_object.id;
7529
7530        let line_ctor = LineCtor {
7531            start: Point2d {
7532                x: Expr::Number(Number {
7533                    value: 0.0,
7534                    units: NumericSuffix::Mm,
7535                }),
7536                y: Expr::Number(Number {
7537                    value: 0.0,
7538                    units: NumericSuffix::Mm,
7539                }),
7540            },
7541            end: Point2d {
7542                x: Expr::Number(Number {
7543                    value: 10.0,
7544                    units: NumericSuffix::Mm,
7545                }),
7546                y: Expr::Number(Number {
7547                    value: 10.0,
7548                    units: NumericSuffix::Mm,
7549                }),
7550            },
7551            construction: None,
7552        };
7553        let segment = SegmentCtor::Line(line_ctor);
7554        let (src_delta, scene_delta) = frontend
7555            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7556            .await
7557            .unwrap();
7558        assert_eq!(
7559            src_delta.text.as_str(),
7560            "s = sketch(on = XY) {
7561  line(start = [0mm, 0mm], end = [10mm, 10mm])
7562}
7563"
7564        );
7565        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7566        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7567
7568        ctx.close().await;
7569        mock_ctx.close().await;
7570    }
7571
7572    #[tokio::test(flavor = "multi_thread")]
7573    async fn test_new_sketch_add_line_delete_sketch() {
7574        let program = Program::empty();
7575
7576        let mut frontend = FrontendState::new();
7577        frontend.program = program;
7578
7579        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7580        let mock_ctx = ExecutorContext::new_mock(None).await;
7581        let version = Version(0);
7582
7583        let sketch_args = SketchCtor {
7584            on: Plane::Default(PlaneName::Xy),
7585        };
7586        let (_src_delta, scene_delta, sketch_id) = frontend
7587            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7588            .await
7589            .unwrap();
7590        assert_eq!(sketch_id, ObjectId(1));
7591        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7592        let sketch_object = &scene_delta.new_graph.objects[1];
7593        assert_eq!(sketch_object.id, ObjectId(1));
7594        assert_eq!(
7595            sketch_object.kind,
7596            ObjectKind::Sketch(Sketch {
7597                args: SketchCtor {
7598                    on: Plane::Default(PlaneName::Xy)
7599                },
7600                plane: ObjectId(0),
7601                segments: vec![],
7602                constraints: vec![],
7603            })
7604        );
7605        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7606
7607        let line_ctor = LineCtor {
7608            start: Point2d {
7609                x: Expr::Number(Number {
7610                    value: 0.0,
7611                    units: NumericSuffix::Mm,
7612                }),
7613                y: Expr::Number(Number {
7614                    value: 0.0,
7615                    units: NumericSuffix::Mm,
7616                }),
7617            },
7618            end: Point2d {
7619                x: Expr::Number(Number {
7620                    value: 10.0,
7621                    units: NumericSuffix::Mm,
7622                }),
7623                y: Expr::Number(Number {
7624                    value: 10.0,
7625                    units: NumericSuffix::Mm,
7626                }),
7627            },
7628            construction: None,
7629        };
7630        let segment = SegmentCtor::Line(line_ctor);
7631        let (src_delta, scene_delta) = frontend
7632            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7633            .await
7634            .unwrap();
7635        assert_eq!(
7636            src_delta.text.as_str(),
7637            "sketch001 = sketch(on = XY) {
7638  line(start = [0mm, 0mm], end = [10mm, 10mm])
7639}
7640"
7641        );
7642        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7643
7644        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7645        assert_eq!(src_delta.text.as_str(), "");
7646        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7647
7648        ctx.close().await;
7649        mock_ctx.close().await;
7650    }
7651
7652    #[tokio::test(flavor = "multi_thread")]
7653    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7654        let initial_source = "s = sketch(on = XY) {}
7655";
7656
7657        let program = Program::parse(initial_source).unwrap().0.unwrap();
7658
7659        let mut frontend = FrontendState::new();
7660
7661        let ctx = ExecutorContext::new_with_engine(
7662            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7663            Default::default(),
7664        );
7665        let version = Version(0);
7666
7667        frontend.hack_set_program(&ctx, program).await.unwrap();
7668        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7669        let sketch_id = sketch_object.id;
7670
7671        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7672        assert_eq!(src_delta.text.as_str(), "");
7673        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7674
7675        ctx.close().await;
7676    }
7677
7678    #[tokio::test(flavor = "multi_thread")]
7679    async fn test_delete_sketch_after_comment() {
7680        let initial_source = "sketch001 = sketch(on = XZ) {
7681}
7682";
7683
7684        let program = Program::parse(initial_source).unwrap().0.unwrap();
7685        let mut frontend = FrontendState::new();
7686
7687        let ctx = ExecutorContext::new_with_engine(
7688            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7689            Default::default(),
7690        );
7691        let version = Version(0);
7692
7693        frontend.hack_set_program(&ctx, program).await.unwrap();
7694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7695        let sketch_id = sketch_object.id;
7696        let original_source = sketch_object.source.clone();
7697
7698        let commented_source = "// test 1
7699sketch001 = sketch(on = XZ) {
7700}
7701";
7702        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7703        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7704
7705        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7706        assert_eq!(cached_sketch_object.source, original_source);
7707
7708        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7709        assert!(
7710            !src_delta.text.contains("sketch001"),
7711            "sketch was not deleted: {}",
7712            src_delta.text
7713        );
7714        // The leading line comment must survive deletion.
7715        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7716        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7717
7718        ctx.close().await;
7719    }
7720
7721    #[tokio::test(flavor = "multi_thread")]
7722    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7723        let initial_source = "sketch001 = sketch(on = XZ) {
7724}
7725foo = 1
7726";
7727
7728        let program = Program::parse(initial_source).unwrap().0.unwrap();
7729        let mut frontend = FrontendState::new();
7730
7731        let ctx = ExecutorContext::new_with_engine(
7732            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7733            Default::default(),
7734        );
7735        let version = Version(0);
7736
7737        frontend.hack_set_program(&ctx, program).await.unwrap();
7738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739        let sketch_id = sketch_object.id;
7740
7741        let commented_source = "// keep me
7742sketch001 = sketch(on = XZ) {
7743}
7744foo = 1
7745";
7746        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7747        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7748
7749        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7750        // The leading comment should remain, now attached to the following body item.
7751        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7752
7753        ctx.close().await;
7754    }
7755
7756    #[tokio::test(flavor = "multi_thread")]
7757    async fn test_delete_segment_preserves_pre_comment() {
7758        let initial_source = "\
7759sketch(on = XY) {
7760  point(at = [var 1, var 2])
7761  // describe the middle point
7762  point(at = [var 3, var 4])
7763  point(at = [var 5, var 6])
7764}
7765";
7766
7767        let program = Program::parse(initial_source).unwrap().0.unwrap();
7768        let mut frontend = FrontendState::new();
7769
7770        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7771        let mock_ctx = ExecutorContext::new_mock(None).await;
7772        let version = Version(0);
7773
7774        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7775        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7776        let sketch_id = sketch_object.id;
7777        let sketch = expect_sketch(sketch_object);
7778
7779        let middle_point_id = *sketch.segments.get(1).unwrap();
7780
7781        let (src_delta, _scene_delta) = frontend
7782            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7783            .await
7784            .unwrap();
7785        // The line comment on the line above the deleted point must be preserved.
7786        // It is reattached to the next surviving body item.
7787        assert_eq!(
7788            src_delta.text.as_str(),
7789            "\
7790sketch(on = XY) {
7791  point(at = [var 1mm, var 2mm])
7792  // describe the middle point
7793  point(at = [var 5mm, var 6mm])
7794}
7795"
7796        );
7797
7798        ctx.close().await;
7799        mock_ctx.close().await;
7800    }
7801
7802    #[tokio::test(flavor = "multi_thread")]
7803    async fn test_delete_last_segment_preserves_pre_comment() {
7804        let initial_source = "\
7805sketch(on = XY) {
7806  point(at = [var 1, var 2])
7807  // describe the trailing point
7808  point(at = [var 3, var 4])
7809}
7810";
7811
7812        let program = Program::parse(initial_source).unwrap().0.unwrap();
7813        let mut frontend = FrontendState::new();
7814
7815        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7816        let mock_ctx = ExecutorContext::new_mock(None).await;
7817        let version = Version(0);
7818
7819        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7820        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7821        let sketch_id = sketch_object.id;
7822        let sketch = expect_sketch(sketch_object);
7823
7824        let last_point_id = *sketch.segments.last().unwrap();
7825
7826        let (src_delta, _scene_delta) = frontend
7827            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7828            .await
7829            .unwrap();
7830        // No following item to attach to; the comment is kept inside the sketch
7831        // block as trailing non-code metadata so the user does not lose it.
7832        assert_eq!(
7833            src_delta.text.as_str(),
7834            "\
7835sketch(on = XY) {
7836  point(at = [var 1mm, var 2mm])
7837  // describe the trailing point
7838}
7839"
7840        );
7841
7842        ctx.close().await;
7843        mock_ctx.close().await;
7844    }
7845
7846    #[tokio::test(flavor = "multi_thread")]
7847    async fn test_delete_segment_drops_inline_trailing_comment() {
7848        let initial_source = "\
7849sketch(on = XY) {
7850  point(at = [var 1, var 2])
7851  point(at = [var 3, var 4]) // same-line note that gets dropped
7852  point(at = [var 5, var 6])
7853}
7854";
7855
7856        let program = Program::parse(initial_source).unwrap().0.unwrap();
7857        let mut frontend = FrontendState::new();
7858
7859        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7860        let mock_ctx = ExecutorContext::new_mock(None).await;
7861        let version = Version(0);
7862
7863        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7865        let sketch_id = sketch_object.id;
7866        let sketch = expect_sketch(sketch_object);
7867
7868        let middle_point_id = *sketch.segments.get(1).unwrap();
7869
7870        let (src_delta, _scene_delta) = frontend
7871            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7872            .await
7873            .unwrap();
7874        // The same-line trailing comment is removed along with the deleted code.
7875        assert!(
7876            !src_delta.text.contains("same-line note"),
7877            "inline comment should have been removed: {}",
7878            src_delta.text
7879        );
7880
7881        ctx.close().await;
7882        mock_ctx.close().await;
7883    }
7884
7885    #[tokio::test(flavor = "multi_thread")]
7886    async fn test_delete_segments_preserves_block_comments_across_positions() {
7887        // One test exercising several `delete_body_item_preserving_pre_comments`
7888        // branches at once with `/* ... */` block comments:
7889        //   - first point: leading block comment must migrate to the next item.
7890        //   - first point: same-line trailing block comment must be dropped.
7891        //   - middle point: leading block comment must stay attached after migration.
7892        //   - last point: leading block comment, with no surviving next item,
7893        //     must be converted into a trailing NonCodeNode.
7894        let initial_source = "\
7895sketch(on = XY) {
7896  /* above first - moves to middle */
7897  point(at = [var 1, var 2]) /* same-line on first - dropped */
7898  /* above middle - stays */
7899  point(at = [var 3, var 4])
7900  /* above last - moves to trailing meta */
7901  point(at = [var 5, var 6])
7902}
7903";
7904
7905        let program = Program::parse(initial_source).unwrap().0.unwrap();
7906        let mut frontend = FrontendState::new();
7907
7908        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7909        let mock_ctx = ExecutorContext::new_mock(None).await;
7910        let version = Version(0);
7911
7912        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7913        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7914        let sketch_id = sketch_object.id;
7915        let sketch = expect_sketch(sketch_object);
7916
7917        let first_point_id = *sketch.segments.first().unwrap();
7918        let last_point_id = *sketch.segments.last().unwrap();
7919
7920        let (src_delta, _scene_delta) = frontend
7921            .delete_objects(
7922                &mock_ctx,
7923                version,
7924                sketch_id,
7925                Vec::new(),
7926                vec![first_point_id, last_point_id],
7927            )
7928            .await
7929            .unwrap();
7930        assert_eq!(
7931            src_delta.text.as_str(),
7932            "\
7933sketch(on = XY) {
7934  /* above first - moves to middle */
7935  /* above middle - stays */
7936  point(at = [var 3mm, var 4mm])
7937  /* above last - moves to trailing meta */
7938}
7939"
7940        );
7941
7942        ctx.close().await;
7943        mock_ctx.close().await;
7944    }
7945
7946    #[tokio::test(flavor = "multi_thread")]
7947    async fn test_edit_line_when_editing_its_start_point() {
7948        let initial_source = "\
7949sketch(on = XY) {
7950  line(start = [var 1, var 2], end = [var 3, var 4])
7951}
7952";
7953
7954        let program = Program::parse(initial_source).unwrap().0.unwrap();
7955
7956        let mut frontend = FrontendState::new();
7957
7958        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7959        let mock_ctx = ExecutorContext::new_mock(None).await;
7960        let version = Version(0);
7961
7962        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7963        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7964        let sketch_id = sketch_object.id;
7965        let sketch = expect_sketch(sketch_object);
7966
7967        let point_id = *sketch.segments.first().unwrap();
7968
7969        let point_ctor = PointCtor {
7970            position: Point2d {
7971                x: Expr::Var(Number {
7972                    value: 5.0,
7973                    units: NumericSuffix::Inch,
7974                }),
7975                y: Expr::Var(Number {
7976                    value: 6.0,
7977                    units: NumericSuffix::Inch,
7978                }),
7979            },
7980        };
7981        let segments = vec![ExistingSegmentCtor {
7982            id: point_id,
7983            ctor: SegmentCtor::Point(point_ctor),
7984        }];
7985        let (src_delta, scene_delta) = frontend
7986            .edit_segments(&mock_ctx, version, sketch_id, segments)
7987            .await
7988            .unwrap();
7989        assert_eq!(
7990            src_delta.text.as_str(),
7991            "\
7992sketch(on = XY) {
7993  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7994}
7995"
7996        );
7997        assert_eq!(scene_delta.new_objects, vec![]);
7998        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7999
8000        ctx.close().await;
8001        mock_ctx.close().await;
8002    }
8003
8004    #[tokio::test(flavor = "multi_thread")]
8005    async fn test_edit_line_when_editing_its_end_point() {
8006        let initial_source = "\
8007sketch(on = XY) {
8008  line(start = [var 1, var 2], end = [var 3, var 4])
8009}
8010";
8011
8012        let program = Program::parse(initial_source).unwrap().0.unwrap();
8013
8014        let mut frontend = FrontendState::new();
8015
8016        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8017        let mock_ctx = ExecutorContext::new_mock(None).await;
8018        let version = Version(0);
8019
8020        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8021        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8022        let sketch_id = sketch_object.id;
8023        let sketch = expect_sketch(sketch_object);
8024        let point_id = *sketch.segments.get(1).unwrap();
8025
8026        let point_ctor = PointCtor {
8027            position: Point2d {
8028                x: Expr::Var(Number {
8029                    value: 5.0,
8030                    units: NumericSuffix::Inch,
8031                }),
8032                y: Expr::Var(Number {
8033                    value: 6.0,
8034                    units: NumericSuffix::Inch,
8035                }),
8036            },
8037        };
8038        let segments = vec![ExistingSegmentCtor {
8039            id: point_id,
8040            ctor: SegmentCtor::Point(point_ctor),
8041        }];
8042        let (src_delta, scene_delta) = frontend
8043            .edit_segments(&mock_ctx, version, sketch_id, segments)
8044            .await
8045            .unwrap();
8046        assert_eq!(
8047            src_delta.text.as_str(),
8048            "\
8049sketch(on = XY) {
8050  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
8051}
8052"
8053        );
8054        assert_eq!(scene_delta.new_objects, vec![]);
8055        assert_eq!(
8056            scene_delta.new_graph.objects.len(),
8057            5,
8058            "{:#?}",
8059            scene_delta.new_graph.objects
8060        );
8061
8062        ctx.close().await;
8063        mock_ctx.close().await;
8064    }
8065
8066    #[tokio::test(flavor = "multi_thread")]
8067    async fn test_edit_line_with_coincident_feedback() {
8068        let initial_source = "\
8069sketch(on = XY) {
8070  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8071  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8072  fixed([line1.start, [0, 0]])
8073  coincident([line1.end, line2.start])
8074  equalLength([line1, line2])
8075}
8076";
8077
8078        let program = Program::parse(initial_source).unwrap().0.unwrap();
8079
8080        let mut frontend = FrontendState::new();
8081
8082        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8083        let mock_ctx = ExecutorContext::new_mock(None).await;
8084        let version = Version(0);
8085
8086        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8088        let sketch_id = sketch_object.id;
8089        let sketch = expect_sketch(sketch_object);
8090        let line2_end_id = *sketch.segments.get(4).unwrap();
8091
8092        let segments = vec![ExistingSegmentCtor {
8093            id: line2_end_id,
8094            ctor: SegmentCtor::Point(PointCtor {
8095                position: Point2d {
8096                    x: Expr::Var(Number {
8097                        value: 9.0,
8098                        units: NumericSuffix::None,
8099                    }),
8100                    y: Expr::Var(Number {
8101                        value: 10.0,
8102                        units: NumericSuffix::None,
8103                    }),
8104                },
8105            }),
8106        }];
8107        let (src_delta, scene_delta) = frontend
8108            .edit_segments(&mock_ctx, version, sketch_id, segments)
8109            .await
8110            .unwrap();
8111        assert_eq!(
8112            src_delta.text.as_str(),
8113            "\
8114sketch(on = XY) {
8115  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
8116  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
8117  fixed([line1.start, [0, 0]])
8118  coincident([line1.end, line2.start])
8119  equalLength([line1, line2])
8120}
8121"
8122        );
8123        assert_eq!(
8124            scene_delta.new_graph.objects.len(),
8125            11,
8126            "{:#?}",
8127            scene_delta.new_graph.objects
8128        );
8129
8130        ctx.close().await;
8131        mock_ctx.close().await;
8132    }
8133
8134    #[tokio::test(flavor = "multi_thread")]
8135    async fn test_delete_point_without_var() {
8136        let initial_source = "\
8137sketch(on = XY) {
8138  point(at = [var 1, var 2])
8139  point(at = [var 3, var 4])
8140  point(at = [var 5, var 6])
8141}
8142";
8143
8144        let program = Program::parse(initial_source).unwrap().0.unwrap();
8145
8146        let mut frontend = FrontendState::new();
8147
8148        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8149        let mock_ctx = ExecutorContext::new_mock(None).await;
8150        let version = Version(0);
8151
8152        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8153        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8154        let sketch_id = sketch_object.id;
8155        let sketch = expect_sketch(sketch_object);
8156
8157        let point_id = *sketch.segments.get(1).unwrap();
8158
8159        let (src_delta, scene_delta) = frontend
8160            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8161            .await
8162            .unwrap();
8163        assert_eq!(
8164            src_delta.text.as_str(),
8165            "\
8166sketch(on = XY) {
8167  point(at = [var 1mm, var 2mm])
8168  point(at = [var 5mm, var 6mm])
8169}
8170"
8171        );
8172        assert_eq!(scene_delta.new_objects, vec![]);
8173        assert_eq!(scene_delta.new_graph.objects.len(), 4);
8174
8175        ctx.close().await;
8176        mock_ctx.close().await;
8177    }
8178
8179    #[tokio::test(flavor = "multi_thread")]
8180    async fn test_delete_point_with_var() {
8181        let initial_source = "\
8182sketch(on = XY) {
8183  point(at = [var 1, var 2])
8184  point1 = point(at = [var 3, var 4])
8185  point(at = [var 5, var 6])
8186}
8187";
8188
8189        let program = Program::parse(initial_source).unwrap().0.unwrap();
8190
8191        let mut frontend = FrontendState::new();
8192
8193        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8194        let mock_ctx = ExecutorContext::new_mock(None).await;
8195        let version = Version(0);
8196
8197        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8198        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8199        let sketch_id = sketch_object.id;
8200        let sketch = expect_sketch(sketch_object);
8201
8202        let point_id = *sketch.segments.get(1).unwrap();
8203
8204        let (src_delta, scene_delta) = frontend
8205            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8206            .await
8207            .unwrap();
8208        assert_eq!(
8209            src_delta.text.as_str(),
8210            "\
8211sketch(on = XY) {
8212  point(at = [var 1mm, var 2mm])
8213  point(at = [var 5mm, var 6mm])
8214}
8215"
8216        );
8217        assert_eq!(scene_delta.new_objects, vec![]);
8218        assert_eq!(scene_delta.new_graph.objects.len(), 4);
8219
8220        ctx.close().await;
8221        mock_ctx.close().await;
8222    }
8223
8224    #[tokio::test(flavor = "multi_thread")]
8225    async fn test_delete_multiple_points() {
8226        let initial_source = "\
8227sketch(on = XY) {
8228  point(at = [var 1, var 2])
8229  point1 = point(at = [var 3, var 4])
8230  point(at = [var 5, var 6])
8231}
8232";
8233
8234        let program = Program::parse(initial_source).unwrap().0.unwrap();
8235
8236        let mut frontend = FrontendState::new();
8237
8238        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8239        let mock_ctx = ExecutorContext::new_mock(None).await;
8240        let version = Version(0);
8241
8242        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8243        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8244        let sketch_id = sketch_object.id;
8245
8246        let sketch = expect_sketch(sketch_object);
8247
8248        let point1_id = *sketch.segments.first().unwrap();
8249        let point2_id = *sketch.segments.get(1).unwrap();
8250
8251        let (src_delta, scene_delta) = frontend
8252            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
8253            .await
8254            .unwrap();
8255        assert_eq!(
8256            src_delta.text.as_str(),
8257            "\
8258sketch(on = XY) {
8259  point(at = [var 5mm, var 6mm])
8260}
8261"
8262        );
8263        assert_eq!(scene_delta.new_objects, vec![]);
8264        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8265
8266        ctx.close().await;
8267        mock_ctx.close().await;
8268    }
8269
8270    #[tokio::test(flavor = "multi_thread")]
8271    async fn test_delete_coincident_constraint() {
8272        let initial_source = "\
8273sketch(on = XY) {
8274  point1 = point(at = [var 1, var 2])
8275  point2 = point(at = [var 3, var 4])
8276  coincident([point1, point2])
8277  point(at = [var 5, var 6])
8278}
8279";
8280
8281        let program = Program::parse(initial_source).unwrap().0.unwrap();
8282
8283        let mut frontend = FrontendState::new();
8284
8285        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8286        let mock_ctx = ExecutorContext::new_mock(None).await;
8287        let version = Version(0);
8288
8289        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8290        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8291        let sketch_id = sketch_object.id;
8292        let sketch = expect_sketch(sketch_object);
8293
8294        let coincident_id = *sketch.constraints.first().unwrap();
8295
8296        let (src_delta, scene_delta) = frontend
8297            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8298            .await
8299            .unwrap();
8300        assert_eq!(
8301            src_delta.text.as_str(),
8302            "\
8303sketch(on = XY) {
8304  point1 = point(at = [var 1mm, var 2mm])
8305  point2 = point(at = [var 3mm, var 4mm])
8306  point(at = [var 5mm, var 6mm])
8307}
8308"
8309        );
8310        assert_eq!(scene_delta.new_objects, vec![]);
8311        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8312
8313        ctx.close().await;
8314        mock_ctx.close().await;
8315    }
8316
8317    #[tokio::test(flavor = "multi_thread")]
8318    async fn test_delete_line_cascades_to_coincident_constraint() {
8319        let initial_source = "\
8320sketch(on = XY) {
8321  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8322  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8323  coincident([line1.end, line2.start])
8324}
8325";
8326
8327        let program = Program::parse(initial_source).unwrap().0.unwrap();
8328
8329        let mut frontend = FrontendState::new();
8330
8331        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8332        let mock_ctx = ExecutorContext::new_mock(None).await;
8333        let version = Version(0);
8334
8335        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8336        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8337        let sketch_id = sketch_object.id;
8338        let sketch = expect_sketch(sketch_object);
8339        let line_id = *sketch.segments.get(5).unwrap();
8340
8341        let (src_delta, scene_delta) = frontend
8342            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8343            .await
8344            .unwrap();
8345        assert_eq!(
8346            src_delta.text.as_str(),
8347            "\
8348sketch(on = XY) {
8349  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8350}
8351"
8352        );
8353        assert_eq!(
8354            scene_delta.new_graph.objects.len(),
8355            5,
8356            "{:#?}",
8357            scene_delta.new_graph.objects
8358        );
8359
8360        ctx.close().await;
8361        mock_ctx.close().await;
8362    }
8363
8364    #[tokio::test(flavor = "multi_thread")]
8365    async fn test_delete_line_cascades_to_distance_constraint() {
8366        let initial_source = "\
8367sketch(on = XY) {
8368  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8369  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8370  distance([line1.end, line2.start]) == 10mm
8371}
8372";
8373
8374        let program = Program::parse(initial_source).unwrap().0.unwrap();
8375
8376        let mut frontend = FrontendState::new();
8377
8378        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8379        let mock_ctx = ExecutorContext::new_mock(None).await;
8380        let version = Version(0);
8381
8382        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8383        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8384        let sketch_id = sketch_object.id;
8385        let sketch = expect_sketch(sketch_object);
8386        let line_id = *sketch.segments.get(5).unwrap();
8387
8388        let (src_delta, scene_delta) = frontend
8389            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8390            .await
8391            .unwrap();
8392        assert_eq!(
8393            src_delta.text.as_str(),
8394            "\
8395sketch(on = XY) {
8396  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8397}
8398"
8399        );
8400        assert_eq!(
8401            scene_delta.new_graph.objects.len(),
8402            5,
8403            "{:#?}",
8404            scene_delta.new_graph.objects
8405        );
8406
8407        ctx.close().await;
8408        mock_ctx.close().await;
8409    }
8410
8411    #[tokio::test(flavor = "multi_thread")]
8412    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
8413        let initial_source = "\
8414sketch(on = XY) {
8415  point1 = point(at = [var 1, var 2])
8416  point2 = point(at = [var 3, var 4])
8417  horizontalDistance([point1, point2]) == 10mm
8418}
8419";
8420
8421        let program = Program::parse(initial_source).unwrap().0.unwrap();
8422
8423        let mut frontend = FrontendState::new();
8424
8425        let mock_ctx = ExecutorContext::new_mock(None).await;
8426        let version = Version(0);
8427
8428        frontend.program = program.clone();
8429        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8430        frontend.update_state_after_exec(outcome, true);
8431        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8432        let sketch_id = sketch_object.id;
8433        let sketch = expect_sketch(sketch_object);
8434        let point2_id = *sketch.segments.get(1).unwrap();
8435
8436        let (src_delta, scene_delta) = frontend
8437            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
8438            .await
8439            .unwrap();
8440        assert_eq!(
8441            src_delta.text.as_str(),
8442            "\
8443sketch(on = XY) {
8444  point1 = point(at = [var 1mm, var 2mm])
8445}
8446"
8447        );
8448        assert_eq!(
8449            scene_delta.new_graph.objects.len(),
8450            3,
8451            "{:#?}",
8452            scene_delta.new_graph.objects
8453        );
8454
8455        mock_ctx.close().await;
8456    }
8457
8458    #[tokio::test(flavor = "multi_thread")]
8459    async fn test_delete_line_cascades_to_fixed_constraint() {
8460        let initial_source = "\
8461sketch(on = XY) {
8462  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8463  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8464  fixed([line1.start, [0, 0]])
8465}
8466";
8467
8468        let program = Program::parse(initial_source).unwrap().0.unwrap();
8469
8470        let mut frontend = FrontendState::new();
8471
8472        let mock_ctx = ExecutorContext::new_mock(None).await;
8473        let version = Version(0);
8474
8475        frontend.program = program.clone();
8476        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8477        frontend.update_state_after_exec(outcome, true);
8478        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8479        let sketch_id = sketch_object.id;
8480        let sketch = expect_sketch(sketch_object);
8481        let line1_id = *sketch.segments.get(2).unwrap();
8482
8483        let (src_delta, scene_delta) = frontend
8484            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8485            .await
8486            .unwrap();
8487        assert_eq!(
8488            src_delta.text.as_str(),
8489            "\
8490sketch(on = XY) {
8491  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8492}
8493"
8494        );
8495        assert_eq!(
8496            scene_delta.new_graph.objects.len(),
8497            5,
8498            "{:#?}",
8499            scene_delta.new_graph.objects
8500        );
8501
8502        mock_ctx.close().await;
8503    }
8504
8505    #[tokio::test(flavor = "multi_thread")]
8506    async fn test_delete_line_cascades_to_midpoint_constraint() {
8507        let initial_source = "\
8508sketch(on = XY) {
8509  point1 = point(at = [var 1, var 2])
8510  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8511  midpoint(line1, point = point1)
8512}
8513";
8514
8515        let program = Program::parse(initial_source).unwrap().0.unwrap();
8516
8517        let mut frontend = FrontendState::new();
8518
8519        let mock_ctx = ExecutorContext::new_mock(None).await;
8520        let version = Version(0);
8521
8522        frontend.program = program.clone();
8523        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8524        frontend.update_state_after_exec(outcome, true);
8525        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8526        let sketch_id = sketch_object.id;
8527        let sketch = expect_sketch(sketch_object);
8528        let line1_id = *sketch.segments.get(3).unwrap();
8529
8530        let (src_delta, scene_delta) = frontend
8531            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8532            .await
8533            .unwrap();
8534        assert_eq!(
8535            src_delta.text.as_str(),
8536            "\
8537sketch(on = XY) {
8538  point1 = point(at = [var 1mm, var 2mm])
8539}
8540"
8541        );
8542        assert_eq!(
8543            scene_delta.new_graph.objects.len(),
8544            3,
8545            "{:#?}",
8546            scene_delta.new_graph.objects
8547        );
8548
8549        mock_ctx.close().await;
8550    }
8551
8552    #[tokio::test(flavor = "multi_thread")]
8553    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8554        let initial_source = "\
8555sketch(on = XY) {
8556  point1 = point(at = [var 1, var 2])
8557  point2 = point(at = [var 3, var 4])
8558  point3 = point(at = [var 5, var 6])
8559  coincident([point1, point2, point3])
8560}
8561";
8562
8563        let program = Program::parse(initial_source).unwrap().0.unwrap();
8564
8565        let mut frontend = FrontendState::new();
8566
8567        let mock_ctx = ExecutorContext::new_mock(None).await;
8568        let version = Version(0);
8569
8570        frontend.program = program.clone();
8571        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8572        frontend.update_state_after_exec(outcome, true);
8573        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8574        let sketch_id = sketch_object.id;
8575        let sketch = expect_sketch(sketch_object);
8576        let point3_id = *sketch.segments.get(2).unwrap();
8577
8578        let (src_delta, scene_delta) = frontend
8579            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8580            .await
8581            .unwrap();
8582        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8583        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8584        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8585        assert!(
8586            src_delta.text.contains("coincident([point1, point2])"),
8587            "{}",
8588            src_delta.text
8589        );
8590
8591        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8592        let sketch = expect_sketch(sketch_object);
8593        assert_eq!(sketch.segments.len(), 2);
8594        assert_eq!(sketch.constraints.len(), 1);
8595
8596        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8597        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8598            panic!("Expected constraint object");
8599        };
8600        let Constraint::Coincident(coincident) = constraint else {
8601            panic!("Expected coincident constraint");
8602        };
8603        assert_eq!(
8604            coincident.segments,
8605            sketch
8606                .segments
8607                .iter()
8608                .copied()
8609                .map(Into::into)
8610                .collect::<Vec<ConstraintSegment>>()
8611        );
8612
8613        mock_ctx.close().await;
8614    }
8615
8616    #[tokio::test(flavor = "multi_thread")]
8617    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8618        let initial_source = "\
8619sketch(on = XY) {
8620  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8621  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8622  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8623  equalLength([line1, line2, line3])
8624}
8625";
8626
8627        let program = Program::parse(initial_source).unwrap().0.unwrap();
8628
8629        let mut frontend = FrontendState::new();
8630
8631        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8632        let mock_ctx = ExecutorContext::new_mock(None).await;
8633        let version = Version(0);
8634
8635        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8636        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8637        let sketch_id = sketch_object.id;
8638        let sketch = expect_sketch(sketch_object);
8639        let line3_id = *sketch.segments.get(8).unwrap();
8640
8641        let (src_delta, scene_delta) = frontend
8642            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8643            .await
8644            .unwrap();
8645        assert_eq!(
8646            src_delta.text.as_str(),
8647            "\
8648sketch(on = XY) {
8649  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8650  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8651  equalLength([line1, line2])
8652}
8653"
8654        );
8655
8656        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8657        let sketch = expect_sketch(sketch_object);
8658        assert_eq!(sketch.constraints.len(), 1);
8659
8660        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8661        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8662            panic!("Expected constraint object");
8663        };
8664        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8665            panic!("Expected lines equal length constraint");
8666        };
8667        assert_eq!(lines_equal_length.lines.len(), 2);
8668
8669        ctx.close().await;
8670        mock_ctx.close().await;
8671    }
8672
8673    #[tokio::test(flavor = "multi_thread")]
8674    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8675        let initial_source = "\
8676sketch(on = XY) {
8677  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8678  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8679  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8680  horizontal([line1.end, line2.start, line3.start])
8681}
8682";
8683
8684        let program = Program::parse(initial_source).unwrap().0.unwrap();
8685
8686        let mut frontend = FrontendState::new();
8687
8688        let mock_ctx = ExecutorContext::new_mock(None).await;
8689        let version = Version(0);
8690
8691        frontend.program = program.clone();
8692        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8693        frontend.update_state_after_exec(outcome, true);
8694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8695        let sketch_id = sketch_object.id;
8696        let sketch = expect_sketch(sketch_object);
8697        let line1_id = *sketch.segments.get(2).unwrap();
8698
8699        let (src_delta, scene_delta) = frontend
8700            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8701            .await
8702            .unwrap();
8703        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8704        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8705        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8706        assert!(
8707            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8708            "{}",
8709            src_delta.text
8710        );
8711
8712        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8713        let sketch = expect_sketch(sketch_object);
8714        assert_eq!(sketch.constraints.len(), 1);
8715
8716        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8717        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8718            panic!("Expected constraint object");
8719        };
8720        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8721            panic!("Expected horizontal points constraint");
8722        };
8723        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8724        assert_eq!(*points, remaining_points);
8725
8726        mock_ctx.close().await;
8727    }
8728
8729    #[tokio::test(flavor = "multi_thread")]
8730    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8731        let initial_source = "\
8732sketch(on = XY) {
8733  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8734  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8735  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8736  vertical([line1.end, line2.start, line3.start])
8737}
8738";
8739
8740        let program = Program::parse(initial_source).unwrap().0.unwrap();
8741
8742        let mut frontend = FrontendState::new();
8743
8744        let mock_ctx = ExecutorContext::new_mock(None).await;
8745        let version = Version(0);
8746
8747        frontend.program = program.clone();
8748        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8749        frontend.update_state_after_exec(outcome, true);
8750        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8751        let sketch_id = sketch_object.id;
8752        let sketch = expect_sketch(sketch_object);
8753        let line1_id = *sketch.segments.get(2).unwrap();
8754
8755        let (src_delta, scene_delta) = frontend
8756            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8757            .await
8758            .unwrap();
8759        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8760        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8761        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8762        assert!(
8763            src_delta.text.contains("vertical([line2.start, line3.start])"),
8764            "{}",
8765            src_delta.text
8766        );
8767
8768        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8769        let sketch = expect_sketch(sketch_object);
8770        assert_eq!(sketch.constraints.len(), 1);
8771
8772        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8773        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8774            panic!("Expected constraint object");
8775        };
8776        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8777            panic!("Expected vertical points constraint");
8778        };
8779        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8780        assert_eq!(*points, remaining_points);
8781
8782        mock_ctx.close().await;
8783    }
8784
8785    #[tokio::test(flavor = "multi_thread")]
8786    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8787        let initial_source = "\
8788sketch(on = XY) {
8789  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8790  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8791  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8792  coincident([line1.end, line2.start, line3.start])
8793}
8794";
8795
8796        let program = Program::parse(initial_source).unwrap().0.unwrap();
8797
8798        let mut frontend = FrontendState::new();
8799
8800        let mock_ctx = ExecutorContext::new_mock(None).await;
8801        let version = Version(0);
8802
8803        frontend.program = program.clone();
8804        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8805        frontend.update_state_after_exec(outcome, true);
8806        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8807        let sketch_id = sketch_object.id;
8808        let sketch = expect_sketch(sketch_object);
8809        let line1_id = *sketch.segments.get(2).unwrap();
8810
8811        let (src_delta, scene_delta) = frontend
8812            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8813            .await
8814            .unwrap();
8815        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8816        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8817        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8818        assert!(
8819            src_delta.text.contains("coincident([line2.start, line3.start])"),
8820            "{}",
8821            src_delta.text
8822        );
8823
8824        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8825        let sketch = expect_sketch(sketch_object);
8826        assert_eq!(sketch.constraints.len(), 1);
8827
8828        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8829        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8830            panic!("Expected constraint object");
8831        };
8832        let Constraint::Coincident(coincident) = constraint else {
8833            panic!("Expected coincident constraint");
8834        };
8835        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8836        assert_eq!(coincident.segments, remaining_segments);
8837
8838        mock_ctx.close().await;
8839    }
8840
8841    #[tokio::test(flavor = "multi_thread")]
8842    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8843        let initial_source = "\
8844sketch(on = XY) {
8845  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8846  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8847  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8848  equalLength([line1, line2, line3])
8849}
8850";
8851
8852        let program = Program::parse(initial_source).unwrap().0.unwrap();
8853
8854        let mut frontend = FrontendState::new();
8855
8856        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8857        let mock_ctx = ExecutorContext::new_mock(None).await;
8858        let version = Version(0);
8859
8860        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8861        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8862        let sketch_id = sketch_object.id;
8863        let sketch = expect_sketch(sketch_object);
8864        let line2_id = *sketch.segments.get(5).unwrap();
8865        let line3_id = *sketch.segments.get(8).unwrap();
8866
8867        let (src_delta, scene_delta) = frontend
8868            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8869            .await
8870            .unwrap();
8871        assert_eq!(
8872            src_delta.text.as_str(),
8873            "\
8874sketch(on = XY) {
8875  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8876}
8877"
8878        );
8879
8880        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8881        let sketch = expect_sketch(sketch_object);
8882        assert!(sketch.constraints.is_empty());
8883
8884        ctx.close().await;
8885        mock_ctx.close().await;
8886    }
8887
8888    #[tokio::test(flavor = "multi_thread")]
8889    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8890        let initial_source = "\
8891sketch(on = XY) {
8892  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8893  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8894  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8895  parallel([line1, line2, line3])
8896}
8897";
8898
8899        let program = Program::parse(initial_source).unwrap().0.unwrap();
8900
8901        let mut frontend = FrontendState::new();
8902
8903        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8904        let mock_ctx = ExecutorContext::new_mock(None).await;
8905        let version = Version(0);
8906
8907        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8908        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8909        let sketch_id = sketch_object.id;
8910        let sketch = expect_sketch(sketch_object);
8911        let line3_id = *sketch.segments.get(8).unwrap();
8912
8913        let (src_delta, scene_delta) = frontend
8914            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8915            .await
8916            .unwrap();
8917        assert_eq!(
8918            src_delta.text.as_str(),
8919            "\
8920sketch(on = XY) {
8921  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8922  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8923  parallel([line1, line2])
8924}
8925"
8926        );
8927
8928        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8929        let sketch = expect_sketch(sketch_object);
8930        assert_eq!(sketch.constraints.len(), 1);
8931
8932        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8933        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8934            panic!("Expected constraint object");
8935        };
8936        let Constraint::Parallel(parallel) = constraint else {
8937            panic!("Expected parallel constraint");
8938        };
8939        assert_eq!(parallel.lines.len(), 2);
8940
8941        ctx.close().await;
8942        mock_ctx.close().await;
8943    }
8944
8945    #[tokio::test(flavor = "multi_thread")]
8946    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8947        let initial_source = "\
8948sketch(on = XY) {
8949  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8950  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8951  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8952  parallel([line1, line2, line3])
8953}
8954";
8955
8956        let program = Program::parse(initial_source).unwrap().0.unwrap();
8957
8958        let mut frontend = FrontendState::new();
8959
8960        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8961        let mock_ctx = ExecutorContext::new_mock(None).await;
8962        let version = Version(0);
8963
8964        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8965        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8966        let sketch_id = sketch_object.id;
8967        let sketch = expect_sketch(sketch_object);
8968        let line2_id = *sketch.segments.get(5).unwrap();
8969        let line3_id = *sketch.segments.get(8).unwrap();
8970
8971        let (src_delta, scene_delta) = frontend
8972            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8973            .await
8974            .unwrap();
8975        assert_eq!(
8976            src_delta.text.as_str(),
8977            "\
8978sketch(on = XY) {
8979  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8980}
8981"
8982        );
8983
8984        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8985        let sketch = expect_sketch(sketch_object);
8986        assert!(sketch.constraints.is_empty());
8987
8988        ctx.close().await;
8989        mock_ctx.close().await;
8990    }
8991
8992    #[tokio::test(flavor = "multi_thread")]
8993    async fn test_delete_line_line_coincident_constraint() {
8994        let initial_source = "\
8995sketch(on = XY) {
8996  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8997  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8998  coincident([line1, line2])
8999}
9000";
9001
9002        let program = Program::parse(initial_source).unwrap().0.unwrap();
9003
9004        let mut frontend = FrontendState::new();
9005
9006        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9007        let mock_ctx = ExecutorContext::new_mock(None).await;
9008        let version = Version(0);
9009
9010        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9011        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9012        let sketch_id = sketch_object.id;
9013        let sketch = expect_sketch(sketch_object);
9014
9015        let coincident_id = *sketch.constraints.first().unwrap();
9016
9017        let (src_delta, scene_delta) = frontend
9018            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9019            .await
9020            .unwrap();
9021        assert_eq!(
9022            src_delta.text.as_str(),
9023            "\
9024sketch(on = XY) {
9025  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
9026  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
9027}
9028"
9029        );
9030        assert_eq!(scene_delta.new_objects, vec![]);
9031        assert_eq!(scene_delta.new_graph.objects.len(), 8);
9032
9033        ctx.close().await;
9034        mock_ctx.close().await;
9035    }
9036
9037    #[tokio::test(flavor = "multi_thread")]
9038    async fn test_two_points_coincident() {
9039        let initial_source = "\
9040sketch(on = XY) {
9041  point1 = point(at = [var 1, var 2])
9042  point(at = [3, 4])
9043}
9044";
9045
9046        let program = Program::parse(initial_source).unwrap().0.unwrap();
9047
9048        let mut frontend = FrontendState::new();
9049
9050        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9051        let mock_ctx = ExecutorContext::new_mock(None).await;
9052        let version = Version(0);
9053
9054        frontend.hack_set_program(&ctx, program).await.unwrap();
9055        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9056        let sketch_id = sketch_object.id;
9057        let sketch = expect_sketch(sketch_object);
9058        let point0_id = *sketch.segments.first().unwrap();
9059        let point1_id = *sketch.segments.get(1).unwrap();
9060
9061        let constraint = Constraint::Coincident(Coincident {
9062            segments: vec![point0_id.into(), point1_id.into()],
9063        });
9064        let (src_delta, scene_delta) = frontend
9065            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9066            .await
9067            .unwrap();
9068        assert_eq!(
9069            src_delta.text.as_str(),
9070            "\
9071sketch(on = XY) {
9072  point1 = point(at = [var 1, var 2])
9073  point2 = point(at = [3, 4])
9074  coincident([point1, point2])
9075}
9076"
9077        );
9078        assert_eq!(
9079            scene_delta.new_graph.objects.len(),
9080            5,
9081            "{:#?}",
9082            scene_delta.new_graph.objects
9083        );
9084
9085        ctx.close().await;
9086        mock_ctx.close().await;
9087    }
9088
9089    #[tokio::test(flavor = "multi_thread")]
9090    async fn test_three_points_coincident() {
9091        let initial_source = "\
9092sketch(on = XY) {
9093  point1 = point(at = [var 1, var 2])
9094  point(at = [var 3, var 4])
9095  point(at = [var 5, var 6])
9096}
9097";
9098
9099        let program = Program::parse(initial_source).unwrap().0.unwrap();
9100
9101        let mut frontend = FrontendState::new();
9102
9103        let mock_ctx = ExecutorContext::new_mock(None).await;
9104        let version = Version(0);
9105
9106        frontend.program = program.clone();
9107        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9108        frontend.update_state_after_exec(outcome, true);
9109        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9110        let sketch_id = sketch_object.id;
9111        let sketch = expect_sketch(sketch_object);
9112        let segments = sketch
9113            .segments
9114            .iter()
9115            .take(3)
9116            .copied()
9117            .map(Into::into)
9118            .collect::<Vec<ConstraintSegment>>();
9119
9120        let constraint = Constraint::Coincident(Coincident {
9121            segments: segments.clone(),
9122        });
9123        let (src_delta, scene_delta) = frontend
9124            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9125            .await
9126            .unwrap();
9127        assert_eq!(
9128            src_delta.text.as_str(),
9129            "\
9130sketch(on = XY) {
9131  point1 = point(at = [var 1, var 2])
9132  point2 = point(at = [var 3, var 4])
9133  point3 = point(at = [var 5, var 6])
9134  coincident([point1, point2, point3])
9135}
9136"
9137        );
9138
9139        let constraint_object = scene_delta
9140            .new_graph
9141            .objects
9142            .iter()
9143            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9144            .unwrap();
9145
9146        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9147            panic!("expected a constraint object");
9148        };
9149
9150        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9151
9152        mock_ctx.close().await;
9153    }
9154
9155    #[tokio::test(flavor = "multi_thread")]
9156    async fn test_source_with_three_point_coincident_tracks_all_segments() {
9157        let initial_source = "\
9158sketch(on = XY) {
9159  point1 = point(at = [var 1, var 2])
9160  point2 = point(at = [var 3, var 4])
9161  point3 = point(at = [var 5, var 6])
9162  coincident([point1, point2, point3])
9163}
9164";
9165
9166        let program = Program::parse(initial_source).unwrap().0.unwrap();
9167
9168        let mut frontend = FrontendState::new();
9169
9170        let ctx = ExecutorContext::new_mock(None).await;
9171        frontend.program = program.clone();
9172        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9173        frontend.update_state_after_exec(outcome, true);
9174
9175        let constraint_object = frontend
9176            .scene_graph
9177            .objects
9178            .iter()
9179            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9180            .unwrap();
9181        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9182            panic!("expected a constraint object");
9183        };
9184
9185        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9186        let sketch = expect_sketch(sketch_object);
9187        let expected_segments = sketch
9188            .segments
9189            .iter()
9190            .take(3)
9191            .copied()
9192            .map(Into::into)
9193            .collect::<Vec<ConstraintSegment>>();
9194
9195        assert_eq!(
9196            constraint,
9197            &Constraint::Coincident(Coincident {
9198                segments: expected_segments,
9199            })
9200        );
9201
9202        ctx.close().await;
9203    }
9204
9205    #[tokio::test(flavor = "multi_thread")]
9206    async fn test_point_origin_coincident_preserves_order() {
9207        let initial_source = "\
9208sketch(on = XY) {
9209  point(at = [var 1, var 2])
9210}
9211";
9212
9213        for (origin_first, expected_source) in [
9214            (
9215                true,
9216                "\
9217sketch(on = XY) {
9218  point1 = point(at = [var 1, var 2])
9219  coincident([ORIGIN, point1])
9220}
9221",
9222            ),
9223            (
9224                false,
9225                "\
9226sketch(on = XY) {
9227  point1 = point(at = [var 1, var 2])
9228  coincident([point1, ORIGIN])
9229}
9230",
9231            ),
9232        ] {
9233            let program = Program::parse(initial_source).unwrap().0.unwrap();
9234
9235            let mut frontend = FrontendState::new();
9236
9237            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9238            let mock_ctx = ExecutorContext::new_mock(None).await;
9239            let version = Version(0);
9240
9241            frontend.hack_set_program(&ctx, program).await.unwrap();
9242            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9243            let sketch_id = sketch_object.id;
9244            let sketch = expect_sketch(sketch_object);
9245            let point_id = *sketch.segments.first().unwrap();
9246
9247            let segments = if origin_first {
9248                vec![ConstraintSegment::ORIGIN, point_id.into()]
9249            } else {
9250                vec![point_id.into(), ConstraintSegment::ORIGIN]
9251            };
9252            let constraint = Constraint::Coincident(Coincident {
9253                segments: segments.clone(),
9254            });
9255            let (src_delta, scene_delta) = frontend
9256                .add_constraint(&mock_ctx, version, sketch_id, constraint)
9257                .await
9258                .unwrap();
9259            assert_eq!(src_delta.text.as_str(), expected_source);
9260
9261            let constraint_object = scene_delta
9262                .new_graph
9263                .objects
9264                .iter()
9265                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9266                .unwrap();
9267
9268            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9269                panic!("expected a constraint object");
9270            };
9271
9272            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9273
9274            ctx.close().await;
9275            mock_ctx.close().await;
9276        }
9277    }
9278
9279    #[tokio::test(flavor = "multi_thread")]
9280    async fn test_coincident_of_line_end_points() {
9281        let initial_source = "\
9282sketch(on = XY) {
9283  line(start = [var 1, var 2], end = [var 3, var 4])
9284  line(start = [var 5, var 6], end = [var 7, var 8])
9285}
9286";
9287
9288        let program = Program::parse(initial_source).unwrap().0.unwrap();
9289
9290        let mut frontend = FrontendState::new();
9291
9292        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9293        let mock_ctx = ExecutorContext::new_mock(None).await;
9294        let version = Version(0);
9295
9296        frontend.hack_set_program(&ctx, program).await.unwrap();
9297        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9298        let sketch_id = sketch_object.id;
9299        let sketch = expect_sketch(sketch_object);
9300        let point0_id = *sketch.segments.get(1).unwrap();
9301        let point1_id = *sketch.segments.get(3).unwrap();
9302
9303        let constraint = Constraint::Coincident(Coincident {
9304            segments: vec![point0_id.into(), point1_id.into()],
9305        });
9306        let (src_delta, scene_delta) = frontend
9307            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9308            .await
9309            .unwrap();
9310        assert_eq!(
9311            src_delta.text.as_str(),
9312            "\
9313sketch(on = XY) {
9314  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9315  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9316  coincident([line1.end, line2.start])
9317}
9318"
9319        );
9320        assert_eq!(
9321            scene_delta.new_graph.objects.len(),
9322            9,
9323            "{:#?}",
9324            scene_delta.new_graph.objects
9325        );
9326
9327        ctx.close().await;
9328        mock_ctx.close().await;
9329    }
9330
9331    #[tokio::test(flavor = "multi_thread")]
9332    async fn test_coincident_of_line_point_and_circle_segment() {
9333        let initial_source = "\
9334sketch(on = XY) {
9335  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9336  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9337}
9338";
9339        let program = Program::parse(initial_source).unwrap().0.unwrap();
9340        let mut frontend = FrontendState::new();
9341
9342        let mock_ctx = ExecutorContext::new_mock(None).await;
9343        let version = Version(0);
9344
9345        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9346        frontend.program = program;
9347        frontend.update_state_after_exec(outcome, true);
9348        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
9349        let sketch_id = sketch_object.id;
9350        let sketch = expect_sketch(sketch_object);
9351
9352        let circle_id = sketch
9353            .segments
9354            .iter()
9355            .copied()
9356            .find(|seg_id| {
9357                matches!(
9358                    &frontend.scene_graph.objects[seg_id.0].kind,
9359                    ObjectKind::Segment {
9360                        segment: Segment::Circle(_)
9361                    }
9362                )
9363            })
9364            .expect("Expected a circle segment in sketch");
9365        let line_id = frontend
9366            .scene_graph
9367            .objects
9368            .iter()
9369            .find_map(|obj| match &obj.kind {
9370                ObjectKind::Segment {
9371                    segment: Segment::Line(line),
9372                } if line.owner.is_none() => Some(obj.id),
9373                _ => None,
9374            })
9375            .expect("Expected a standalone line segment in scene graph");
9376
9377        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
9378            ObjectKind::Segment {
9379                segment: Segment::Line(line),
9380            } => line.start,
9381            _ => panic!("Expected line segment object"),
9382        };
9383
9384        let constraint = Constraint::Coincident(Coincident {
9385            segments: vec![line_start_point_id.into(), circle_id.into()],
9386        });
9387        let (src_delta, _scene_delta) = frontend
9388            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9389            .await
9390            .unwrap();
9391        assert_eq!(
9392            src_delta.text.as_str(),
9393            "\
9394sketch(on = XY) {
9395  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9396  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9397  coincident([line1.start, circle1])
9398}
9399"
9400        );
9401
9402        mock_ctx.close().await;
9403    }
9404
9405    #[tokio::test(flavor = "multi_thread")]
9406    async fn test_invalid_coincident_arc_and_line_preserves_state() {
9407        // Test that attempting an invalid coincident constraint (arc and line)
9408        // doesn't corrupt the state, allowing subsequent operations to work.
9409        // This test verifies the transactional fix in add_constraint that prevents
9410        // state corruption when invalid constraints are attempted.
9411        // Example: coincident constraint between an arc segment and a straight line segment
9412        // is geometrically invalid and should fail, but state should remain intact.
9413        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
9414        let program = Program::empty();
9415
9416        let mut frontend = FrontendState::new();
9417        frontend.program = program;
9418
9419        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9420        let mock_ctx = ExecutorContext::new_mock(None).await;
9421        let version = Version(0);
9422
9423        let sketch_args = SketchCtor {
9424            on: Plane::Default(PlaneName::Xy),
9425        };
9426        let (_src_delta, _scene_delta, sketch_id) = frontend
9427            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9428            .await
9429            .unwrap();
9430
9431        // Add an arc segment
9432        let arc_ctor = ArcCtor {
9433            start: Point2d {
9434                x: Expr::Var(Number {
9435                    value: 0.0,
9436                    units: NumericSuffix::Mm,
9437                }),
9438                y: Expr::Var(Number {
9439                    value: 0.0,
9440                    units: NumericSuffix::Mm,
9441                }),
9442            },
9443            end: Point2d {
9444                x: Expr::Var(Number {
9445                    value: 10.0,
9446                    units: NumericSuffix::Mm,
9447                }),
9448                y: Expr::Var(Number {
9449                    value: 10.0,
9450                    units: NumericSuffix::Mm,
9451                }),
9452            },
9453            center: Point2d {
9454                x: Expr::Var(Number {
9455                    value: 10.0,
9456                    units: NumericSuffix::Mm,
9457                }),
9458                y: Expr::Var(Number {
9459                    value: 0.0,
9460                    units: NumericSuffix::Mm,
9461                }),
9462            },
9463            construction: None,
9464        };
9465        let (_src_delta, scene_delta) = frontend
9466            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
9467            .await
9468            .unwrap();
9469        // The arc is the last object in new_objects (after the 3 points: start, end, center)
9470        let arc_id = *scene_delta.new_objects.last().unwrap();
9471
9472        // Add a line segment
9473        let line_ctor = LineCtor {
9474            start: Point2d {
9475                x: Expr::Var(Number {
9476                    value: 20.0,
9477                    units: NumericSuffix::Mm,
9478                }),
9479                y: Expr::Var(Number {
9480                    value: 0.0,
9481                    units: NumericSuffix::Mm,
9482                }),
9483            },
9484            end: Point2d {
9485                x: Expr::Var(Number {
9486                    value: 30.0,
9487                    units: NumericSuffix::Mm,
9488                }),
9489                y: Expr::Var(Number {
9490                    value: 10.0,
9491                    units: NumericSuffix::Mm,
9492                }),
9493            },
9494            construction: None,
9495        };
9496        let (_src_delta, scene_delta) = frontend
9497            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9498            .await
9499            .unwrap();
9500        // The line is the last object in new_objects (after the 2 points: start, end)
9501        let line_id = *scene_delta.new_objects.last().unwrap();
9502
9503        // Attempt to add an invalid coincident constraint between arc and line
9504        // This should fail during execution, but state should remain intact
9505        let constraint = Constraint::Coincident(Coincident {
9506            segments: vec![arc_id.into(), line_id.into()],
9507        });
9508        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9509
9510        // The constraint addition should fail (invalid constraint)
9511        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9512
9513        // Verify state is not corrupted by checking that we can still access the scene graph
9514        // and that the original segments are still present with their source ranges
9515        let sketch_object_after =
9516            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9517        let sketch_after = expect_sketch(sketch_object_after);
9518
9519        // Verify both segments are still in the sketch
9520        assert!(
9521            sketch_after.segments.contains(&arc_id),
9522            "Arc segment should still exist after failed constraint"
9523        );
9524        assert!(
9525            sketch_after.segments.contains(&line_id),
9526            "Line segment should still exist after failed constraint"
9527        );
9528
9529        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
9530        let arc_obj = frontend
9531            .scene_graph
9532            .objects
9533            .get(arc_id.0)
9534            .expect("Arc object should still be accessible");
9535        let line_obj = frontend
9536            .scene_graph
9537            .objects
9538            .get(line_id.0)
9539            .expect("Line object should still be accessible");
9540
9541        // Verify source ranges are still valid (not corrupted)
9542        // Just verify that the objects are still accessible and have the expected types
9543        match &arc_obj.kind {
9544            ObjectKind::Segment {
9545                segment: Segment::Arc(_),
9546            } => {}
9547            _ => panic!("Arc object should still be an arc segment"),
9548        }
9549        match &line_obj.kind {
9550            ObjectKind::Segment {
9551                segment: Segment::Line(_),
9552            } => {}
9553            _ => panic!("Line object should still be a line segment"),
9554        }
9555
9556        ctx.close().await;
9557        mock_ctx.close().await;
9558    }
9559
9560    #[tokio::test(flavor = "multi_thread")]
9561    async fn test_distance_two_points() {
9562        let initial_source = "\
9563sketch(on = XY) {
9564  point(at = [var 1, var 2])
9565  point(at = [var 3, var 4])
9566}
9567";
9568
9569        let program = Program::parse(initial_source).unwrap().0.unwrap();
9570
9571        let mut frontend = FrontendState::new();
9572
9573        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9574        let mock_ctx = ExecutorContext::new_mock(None).await;
9575        let version = Version(0);
9576
9577        frontend.hack_set_program(&ctx, program).await.unwrap();
9578        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9579        let sketch_id = sketch_object.id;
9580        let sketch = expect_sketch(sketch_object);
9581        let point0_id = *sketch.segments.first().unwrap();
9582        let point1_id = *sketch.segments.get(1).unwrap();
9583
9584        let constraint = Constraint::Distance(Distance {
9585            points: vec![point0_id.into(), point1_id.into()],
9586            distance: Number {
9587                value: 2.0,
9588                units: NumericSuffix::Mm,
9589            },
9590            label_position: None,
9591            source: Default::default(),
9592        });
9593        let (src_delta, scene_delta) = frontend
9594            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9595            .await
9596            .unwrap();
9597        assert_eq!(
9598            src_delta.text.as_str(),
9599            // The lack indentation is a formatter bug.
9600            "\
9601sketch(on = XY) {
9602  point1 = point(at = [var 1, var 2])
9603  point2 = point(at = [var 3, var 4])
9604  distance([point1, point2]) == 2mm
9605}
9606"
9607        );
9608        assert_eq!(
9609            scene_delta.new_graph.objects.len(),
9610            5,
9611            "{:#?}",
9612            scene_delta.new_graph.objects
9613        );
9614
9615        ctx.close().await;
9616        mock_ctx.close().await;
9617    }
9618
9619    #[tokio::test(flavor = "multi_thread")]
9620    async fn test_distance_two_points_with_label() {
9621        let initial_source = "\
9622sketch(on = XY) {
9623  point(at = [var 1, var 2])
9624  point(at = [var 3, var 4])
9625}
9626";
9627
9628        let program = Program::parse(initial_source).unwrap().0.unwrap();
9629
9630        let mut frontend = FrontendState::new();
9631
9632        let mock_ctx = ExecutorContext::new_mock(None).await;
9633        let version = Version(0);
9634
9635        frontend.program = program.clone();
9636        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9637        frontend.update_state_after_exec(outcome, true);
9638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9639        let sketch_id = sketch_object.id;
9640        let sketch = expect_sketch(sketch_object);
9641        let point0_id = *sketch.segments.first().unwrap();
9642        let point1_id = *sketch.segments.get(1).unwrap();
9643
9644        let label_position = Point2d {
9645            x: Number {
9646                value: 10.0,
9647                units: NumericSuffix::Mm,
9648            },
9649            y: Number {
9650                value: 11.0,
9651                units: NumericSuffix::Mm,
9652            },
9653        };
9654        let constraint = Constraint::Distance(Distance {
9655            points: vec![point0_id.into(), point1_id.into()],
9656            distance: Number {
9657                value: 2.0,
9658                units: NumericSuffix::Mm,
9659            },
9660            label_position: Some(label_position.clone()),
9661            source: Default::default(),
9662        });
9663        let (src_delta, scene_delta) = frontend
9664            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9665            .await
9666            .unwrap();
9667        assert_eq!(
9668            src_delta.text.as_str(),
9669            "\
9670sketch(on = XY) {
9671  point1 = point(at = [var 1, var 2])
9672  point2 = point(at = [var 3, var 4])
9673  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9674}
9675"
9676        );
9677
9678        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9679        let sketch = expect_sketch(sketch_object);
9680        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9681        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9682            panic!("Expected constraint object");
9683        };
9684        let Constraint::Distance(distance) = constraint else {
9685            panic!("Expected distance constraint");
9686        };
9687        assert_eq!(distance.label_position, Some(label_position));
9688
9689        mock_ctx.close().await;
9690    }
9691
9692    #[tokio::test(flavor = "multi_thread")]
9693    async fn test_edit_distance_constraint_label_position() {
9694        let initial_source = "\
9695sketch(on = XY) {
9696  point(at = [var 1, var 2])
9697  point(at = [var 3, var 2])
9698}
9699";
9700
9701        let program = Program::parse(initial_source).unwrap().0.unwrap();
9702
9703        let mut frontend = FrontendState::new();
9704
9705        let mock_ctx = ExecutorContext::new_mock(None).await;
9706        let version = Version(0);
9707
9708        frontend.program = program.clone();
9709        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9710        frontend.update_state_after_exec(outcome, true);
9711        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9712        let sketch_id = sketch_object.id;
9713        let sketch = expect_sketch(sketch_object);
9714        let point0_id = *sketch.segments.first().unwrap();
9715        let point1_id = *sketch.segments.get(1).unwrap();
9716
9717        let constraint = Constraint::Distance(Distance {
9718            points: vec![point0_id.into(), point1_id.into()],
9719            distance: Number {
9720                value: 2.0,
9721                units: NumericSuffix::Mm,
9722            },
9723            label_position: None,
9724            source: Default::default(),
9725        });
9726        let (_, scene_delta) = frontend
9727            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9728            .await
9729            .unwrap();
9730        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9731        let sketch = expect_sketch(sketch_object);
9732        let constraint_id = sketch.constraints[0];
9733        let label_position = Point2d {
9734            x: Number {
9735                value: 10.0,
9736                units: NumericSuffix::Mm,
9737            },
9738            y: Number {
9739                value: 11.0,
9740                units: NumericSuffix::Mm,
9741            },
9742        };
9743
9744        let (src_delta, scene_delta) = frontend
9745            .edit_distance_constraint_label_position(
9746                &mock_ctx,
9747                version,
9748                sketch_id,
9749                constraint_id,
9750                label_position.clone(),
9751                vec![],
9752            )
9753            .await
9754            .unwrap();
9755        assert_eq!(
9756            src_delta.text.as_str(),
9757            "\
9758sketch(on = XY) {
9759  point1 = point(at = [var 1mm, var 2mm])
9760  point2 = point(at = [var 3mm, var 2mm])
9761  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9762}
9763"
9764        );
9765
9766        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9767        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9768            panic!("Expected constraint object");
9769        };
9770        let Constraint::Distance(distance) = constraint else {
9771            panic!("Expected distance constraint");
9772        };
9773        assert_eq!(distance.label_position, Some(label_position));
9774
9775        mock_ctx.close().await;
9776    }
9777
9778    #[tokio::test(flavor = "multi_thread")]
9779    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9780        let initial_source = "\
9781sketch(on = XY) {
9782  point1 = point(at = [var 0mm, var 0mm])
9783  point2 = point(at = [var 10mm, var 0mm])
9784  distance([point1, point2]) == 5mm
9785}
9786";
9787
9788        let program = Program::parse(initial_source).unwrap().0.unwrap();
9789        let mut frontend = FrontendState::new();
9790        let mock_ctx = ExecutorContext::new_mock(None).await;
9791        let version = Version(0);
9792
9793        frontend.program = program.clone();
9794        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9795        frontend.update_state_after_exec(outcome, true);
9796        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9797        let sketch_id = sketch_object.id;
9798        let sketch = expect_sketch(sketch_object);
9799        let point0_id = sketch.segments[0];
9800        let point1_id = sketch.segments[1];
9801        let constraint_id = sketch.constraints[0];
9802
9803        let edited_segments = vec![ExistingSegmentCtor {
9804            id: point0_id,
9805            ctor: SegmentCtor::Point(PointCtor {
9806                position: Point2d {
9807                    x: Expr::Var(Number {
9808                        value: 2.0,
9809                        units: NumericSuffix::Mm,
9810                    }),
9811                    y: Expr::Var(Number {
9812                        value: 1.0,
9813                        units: NumericSuffix::Mm,
9814                    }),
9815                },
9816            }),
9817        }];
9818        let (_, scene_delta) = frontend
9819            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9820            .await
9821            .unwrap();
9822        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9823        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9824
9825        let label_position = Point2d {
9826            x: Number {
9827                value: 3.0,
9828                units: NumericSuffix::Mm,
9829            },
9830            y: Number {
9831                value: 4.0,
9832                units: NumericSuffix::Mm,
9833            },
9834        };
9835        let (_, scene_delta) = frontend
9836            .edit_distance_constraint_label_position(
9837                &mock_ctx,
9838                version,
9839                sketch_id,
9840                constraint_id,
9841                label_position,
9842                vec![point0_id],
9843            )
9844            .await
9845            .unwrap();
9846
9847        assert_point_position_close(
9848            point_position(&scene_delta.new_graph, point0_id),
9849            point0_after_segment_edit,
9850        );
9851        assert_point_position_close(
9852            point_position(&scene_delta.new_graph, point1_id),
9853            point1_after_segment_edit,
9854        );
9855
9856        mock_ctx.close().await;
9857    }
9858
9859    #[tokio::test(flavor = "multi_thread")]
9860    async fn test_distance_point_line() {
9861        let initial_source = "\
9862sketch(on = XY) {
9863  point(at = [var 0, var 5])
9864  line(start = [var 0, var 0], end = [var 10, var 0])
9865}
9866";
9867
9868        let program = Program::parse(initial_source).unwrap().0.unwrap();
9869
9870        let mut frontend = FrontendState::new();
9871
9872        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9873        let mock_ctx = ExecutorContext::new_mock(None).await;
9874        let version = Version(0);
9875
9876        frontend.hack_set_program(&ctx, program).await.unwrap();
9877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878        let sketch_id = sketch_object.id;
9879        let sketch = expect_sketch(sketch_object);
9880        let point_id = *sketch.segments.first().unwrap();
9881        let line_id = *sketch
9882            .segments
9883            .iter()
9884            .find(|segment_id| {
9885                matches!(
9886                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9887                    Some(ObjectKind::Segment {
9888                        segment: Segment::Line(_)
9889                    })
9890                )
9891            })
9892            .unwrap();
9893
9894        let label_position = Point2d {
9895            x: Number {
9896                value: 10.0,
9897                units: NumericSuffix::Mm,
9898            },
9899            y: Number {
9900                value: 11.0,
9901                units: NumericSuffix::Mm,
9902            },
9903        };
9904        let constraint = Constraint::Distance(Distance {
9905            points: vec![point_id.into(), line_id.into()],
9906            distance: Number {
9907                value: 5.0,
9908                units: NumericSuffix::Mm,
9909            },
9910            label_position: Some(label_position.clone()),
9911            source: Default::default(),
9912        });
9913        let (src_delta, scene_delta) = frontend
9914            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9915            .await
9916            .unwrap();
9917        assert_eq!(
9918            src_delta.text.as_str(),
9919            "\
9920sketch(on = XY) {
9921  point1 = point(at = [var 0, var 5])
9922  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9923  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9924}
9925"
9926        );
9927        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9928        let sketch = expect_sketch(sketch_object);
9929        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9930        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9931            panic!("Expected constraint object");
9932        };
9933        let Constraint::Distance(distance) = constraint else {
9934            panic!("Expected distance constraint");
9935        };
9936        assert_eq!(distance.label_position, Some(label_position));
9937
9938        ctx.close().await;
9939        mock_ctx.close().await;
9940    }
9941
9942    #[tokio::test(flavor = "multi_thread")]
9943    async fn test_distance_point_arc() {
9944        let initial_source = "\
9945sketch(on = XY) {
9946  point(at = [var 0, var 8])
9947  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9948}
9949";
9950
9951        let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953        let mut frontend = FrontendState::new();
9954
9955        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9956        let mock_ctx = ExecutorContext::new_mock(None).await;
9957        let version = Version(0);
9958
9959        frontend.hack_set_program(&ctx, program).await.unwrap();
9960        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961        let sketch_id = sketch_object.id;
9962        let sketch = expect_sketch(sketch_object);
9963        let point_id = *sketch.segments.first().unwrap();
9964        let arc_id = *sketch
9965            .segments
9966            .iter()
9967            .find(|segment_id| {
9968                matches!(
9969                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9970                    Some(ObjectKind::Segment {
9971                        segment: Segment::Arc(_)
9972                    })
9973                )
9974            })
9975            .unwrap();
9976
9977        let constraint = Constraint::Distance(Distance {
9978            points: vec![point_id.into(), arc_id.into()],
9979            distance: Number {
9980                value: 3.0,
9981                units: NumericSuffix::Mm,
9982            },
9983            label_position: None,
9984            source: Default::default(),
9985        });
9986        let (src_delta, _scene_delta) = frontend
9987            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9988            .await
9989            .unwrap();
9990        assert_eq!(
9991            src_delta.text.as_str(),
9992            "\
9993sketch(on = XY) {
9994  point1 = point(at = [var 0, var 8])
9995  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9996  distance([point1, arc1]) == 3mm
9997}
9998"
9999        );
10000
10001        ctx.close().await;
10002        mock_ctx.close().await;
10003    }
10004
10005    #[tokio::test(flavor = "multi_thread")]
10006    async fn test_distance_arc_origin() {
10007        let initial_source = "\
10008sketch001 = sketch(on = XY) {
10009  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10010}
10011";
10012
10013        let program = Program::parse(initial_source).unwrap().0.unwrap();
10014
10015        let mut frontend = FrontendState::new();
10016
10017        let mock_ctx = ExecutorContext::new_mock(None).await;
10018        let version = Version(0);
10019
10020        frontend.program = program.clone();
10021        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10022        frontend.update_state_after_exec(outcome, true);
10023        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10024        let sketch_id = sketch_object.id;
10025        let sketch = expect_sketch(sketch_object);
10026        let arc_id = *sketch
10027            .segments
10028            .iter()
10029            .find(|segment_id| {
10030                matches!(
10031                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10032                    Some(ObjectKind::Segment {
10033                        segment: Segment::Arc(_)
10034                    })
10035                )
10036            })
10037            .unwrap();
10038
10039        let constraint = Constraint::Distance(Distance {
10040            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10041            distance: Number {
10042                value: 3.0,
10043                units: NumericSuffix::Mm,
10044            },
10045            label_position: None,
10046            source: Default::default(),
10047        });
10048        let (src_delta, _scene_delta) = frontend
10049            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10050            .await
10051            .unwrap();
10052        assert_eq!(
10053            src_delta.text.as_str(),
10054            "\
10055sketch001 = sketch(on = XY) {
10056  arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10057  distance([arc1, ORIGIN]) == 3mm
10058}
10059"
10060        );
10061
10062        mock_ctx.close().await;
10063    }
10064
10065    #[tokio::test(flavor = "multi_thread")]
10066    async fn test_distance_line_origin() {
10067        let initial_source = "\
10068sketch(on = XY) {
10069  line(start = [var 5, var 0], end = [var 5, var 10])
10070}
10071";
10072
10073        let program = Program::parse(initial_source).unwrap().0.unwrap();
10074
10075        let mut frontend = FrontendState::new();
10076
10077        let mock_ctx = ExecutorContext::new_mock(None).await;
10078        let version = Version(0);
10079
10080        frontend.program = program.clone();
10081        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10082        frontend.update_state_after_exec(outcome, true);
10083        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10084        let sketch_id = sketch_object.id;
10085        let sketch = expect_sketch(sketch_object);
10086        let line_id = *sketch
10087            .segments
10088            .iter()
10089            .find(|segment_id| {
10090                matches!(
10091                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10092                    Some(ObjectKind::Segment {
10093                        segment: Segment::Line(_)
10094                    })
10095                )
10096            })
10097            .unwrap();
10098
10099        let constraint = Constraint::Distance(Distance {
10100            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
10101            distance: Number {
10102                value: 5.0,
10103                units: NumericSuffix::Mm,
10104            },
10105            label_position: None,
10106            source: Default::default(),
10107        });
10108        let (src_delta, _scene_delta) = frontend
10109            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10110            .await
10111            .unwrap();
10112        assert_eq!(
10113            src_delta.text.as_str(),
10114            "\
10115sketch(on = XY) {
10116  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
10117  distance([ORIGIN, line1]) == 5mm
10118}
10119"
10120        );
10121
10122        mock_ctx.close().await;
10123    }
10124
10125    #[tokio::test(flavor = "multi_thread")]
10126    async fn test_distance_line_circle() {
10127        let initial_source = "\
10128sketch(on = XY) {
10129  line(start = [var -10, var 8], end = [var 10, var 8])
10130  circle(start = [var 5, var 0], center = [var 0, var 0])
10131}
10132";
10133
10134        let program = Program::parse(initial_source).unwrap().0.unwrap();
10135
10136        let mut frontend = FrontendState::new();
10137
10138        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10139        let mock_ctx = ExecutorContext::new_mock(None).await;
10140        let version = Version(0);
10141
10142        frontend.hack_set_program(&ctx, program).await.unwrap();
10143        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10144        let sketch_id = sketch_object.id;
10145        let sketch = expect_sketch(sketch_object);
10146        let line_id = *sketch
10147            .segments
10148            .iter()
10149            .find(|segment_id| {
10150                matches!(
10151                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10152                    Some(ObjectKind::Segment {
10153                        segment: Segment::Line(_)
10154                    })
10155                )
10156            })
10157            .unwrap();
10158        let circle_id = *sketch
10159            .segments
10160            .iter()
10161            .find(|segment_id| {
10162                matches!(
10163                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10164                    Some(ObjectKind::Segment {
10165                        segment: Segment::Circle(_)
10166                    })
10167                )
10168            })
10169            .unwrap();
10170
10171        let constraint = Constraint::Distance(Distance {
10172            points: vec![line_id.into(), circle_id.into()],
10173            distance: Number {
10174                value: 3.0,
10175                units: NumericSuffix::Mm,
10176            },
10177            label_position: None,
10178            source: Default::default(),
10179        });
10180        let (src_delta, _scene_delta) = frontend
10181            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10182            .await
10183            .unwrap();
10184        assert_eq!(
10185            src_delta.text.as_str(),
10186            "\
10187sketch(on = XY) {
10188  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
10189  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10190  distance([line1, circle1]) == 3mm
10191}
10192"
10193        );
10194
10195        ctx.close().await;
10196        mock_ctx.close().await;
10197    }
10198
10199    #[tokio::test(flavor = "multi_thread")]
10200    async fn test_distance_circle_arc() {
10201        let initial_source = "\
10202sketch(on = XY) {
10203  circle(start = [var 5, var 0], center = [var 0, var 0])
10204  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10205}
10206";
10207
10208        let program = Program::parse(initial_source).unwrap().0.unwrap();
10209
10210        let mut frontend = FrontendState::new();
10211
10212        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10213        let mock_ctx = ExecutorContext::new_mock(None).await;
10214        let version = Version(0);
10215
10216        frontend.hack_set_program(&ctx, program).await.unwrap();
10217        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10218        let sketch_id = sketch_object.id;
10219        let sketch = expect_sketch(sketch_object);
10220        let circle_id = *sketch
10221            .segments
10222            .iter()
10223            .find(|segment_id| {
10224                matches!(
10225                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10226                    Some(ObjectKind::Segment {
10227                        segment: Segment::Circle(_)
10228                    })
10229                )
10230            })
10231            .unwrap();
10232        let arc_id = *sketch
10233            .segments
10234            .iter()
10235            .find(|segment_id| {
10236                matches!(
10237                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10238                    Some(ObjectKind::Segment {
10239                        segment: Segment::Arc(_)
10240                    })
10241                )
10242            })
10243            .unwrap();
10244
10245        let constraint = Constraint::Distance(Distance {
10246            points: vec![circle_id.into(), arc_id.into()],
10247            distance: Number {
10248                value: 3.0,
10249                units: NumericSuffix::Mm,
10250            },
10251            label_position: None,
10252            source: Default::default(),
10253        });
10254        let (src_delta, _scene_delta) = frontend
10255            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10256            .await
10257            .unwrap();
10258        assert_eq!(
10259            src_delta.text.as_str(),
10260            "\
10261sketch(on = XY) {
10262  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10263  arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10264  distance([circle1, arc1]) == 3mm
10265}
10266"
10267        );
10268
10269        ctx.close().await;
10270        mock_ctx.close().await;
10271    }
10272
10273    #[tokio::test(flavor = "multi_thread")]
10274    async fn test_distance_parallel_lines() {
10275        let initial_source = "\
10276sketch(on = XY) {
10277  line(start = [var 0, var 0], end = [var 10, var 0])
10278  line(start = [var 0, var 5], end = [var 10, var 5])
10279}
10280";
10281
10282        let program = Program::parse(initial_source).unwrap().0.unwrap();
10283
10284        let mut frontend = FrontendState::new();
10285
10286        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10287        let mock_ctx = ExecutorContext::new_mock(None).await;
10288        let version = Version(0);
10289
10290        frontend.hack_set_program(&ctx, program).await.unwrap();
10291        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10292        let sketch_id = sketch_object.id;
10293        let sketch = expect_sketch(sketch_object);
10294        let line_ids = sketch
10295            .segments
10296            .iter()
10297            .copied()
10298            .filter(|segment_id| {
10299                matches!(
10300                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10301                    Some(ObjectKind::Segment {
10302                        segment: Segment::Line(_)
10303                    })
10304                )
10305            })
10306            .collect::<Vec<_>>();
10307
10308        let constraint = Constraint::Distance(Distance {
10309            points: vec![line_ids[0].into(), line_ids[1].into()],
10310            distance: Number {
10311                value: 5.0,
10312                units: NumericSuffix::Mm,
10313            },
10314            label_position: None,
10315            source: Default::default(),
10316        });
10317        let (src_delta, _scene_delta) = frontend
10318            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10319            .await
10320            .unwrap();
10321        assert_eq!(
10322            src_delta.text.as_str(),
10323            "\
10324sketch(on = XY) {
10325  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10326  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
10327  distance([line1, line2]) == 5mm
10328}
10329"
10330        );
10331
10332        ctx.close().await;
10333        mock_ctx.close().await;
10334    }
10335
10336    #[tokio::test(flavor = "multi_thread")]
10337    async fn test_distance_non_parallel_lines_lowers_to_distance() {
10338        let initial_source = "\
10339sketch(on = XY) {
10340  line(start = [var 0, var 0], end = [var 10, var 0])
10341  line(start = [var 0, var 0], end = [var 0, var 10])
10342}
10343";
10344
10345        let program = Program::parse(initial_source).unwrap().0.unwrap();
10346
10347        let mut frontend = FrontendState::new();
10348
10349        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10350        let mock_ctx = ExecutorContext::new_mock(None).await;
10351        let version = Version(0);
10352
10353        frontend.hack_set_program(&ctx, program).await.unwrap();
10354        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10355        let sketch_id = sketch_object.id;
10356        let sketch = expect_sketch(sketch_object);
10357        let line_ids = sketch
10358            .segments
10359            .iter()
10360            .copied()
10361            .filter(|segment_id| {
10362                matches!(
10363                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10364                    Some(ObjectKind::Segment {
10365                        segment: Segment::Line(_)
10366                    })
10367                )
10368            })
10369            .collect::<Vec<_>>();
10370
10371        let constraint = Constraint::Distance(Distance {
10372            points: vec![line_ids[0].into(), line_ids[1].into()],
10373            distance: Number {
10374                value: 5.0,
10375                units: NumericSuffix::Mm,
10376            },
10377            label_position: None,
10378            source: Default::default(),
10379        });
10380        let (src_delta, _scene_delta) = frontend
10381            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10382            .await
10383            .unwrap();
10384        assert_eq!(
10385            src_delta.text.as_str(),
10386            "\
10387sketch(on = XY) {
10388  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10389  line2 = line(start = [var 0, var 0], end = [var 0, var 10])
10390  distance([line1, line2]) == 5mm
10391}
10392"
10393        );
10394
10395        ctx.close().await;
10396        mock_ctx.close().await;
10397    }
10398
10399    #[tokio::test(flavor = "multi_thread")]
10400    async fn test_horizontal_distance_two_points() {
10401        let initial_source = "\
10402sketch(on = XY) {
10403  point(at = [var 1, var 2])
10404  point(at = [var 3, var 4])
10405}
10406";
10407
10408        let program = Program::parse(initial_source).unwrap().0.unwrap();
10409
10410        let mut frontend = FrontendState::new();
10411
10412        let mock_ctx = ExecutorContext::new_mock(None).await;
10413        let version = Version(0);
10414
10415        frontend.program = program.clone();
10416        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10417        frontend.update_state_after_exec(outcome, true);
10418        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10419        let sketch_id = sketch_object.id;
10420        let sketch = expect_sketch(sketch_object);
10421        let point0_id = *sketch.segments.first().unwrap();
10422        let point1_id = *sketch.segments.get(1).unwrap();
10423        let label_position = Point2d {
10424            x: Number {
10425                value: 10.0,
10426                units: NumericSuffix::Mm,
10427            },
10428            y: Number {
10429                value: 11.0,
10430                units: NumericSuffix::Mm,
10431            },
10432        };
10433
10434        let constraint = Constraint::HorizontalDistance(Distance {
10435            points: vec![point0_id.into(), point1_id.into()],
10436            distance: Number {
10437                value: 2.0,
10438                units: NumericSuffix::Mm,
10439            },
10440            label_position: Some(label_position.clone()),
10441            source: Default::default(),
10442        });
10443        let (src_delta, scene_delta) = frontend
10444            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10445            .await
10446            .unwrap();
10447        assert_eq!(
10448            src_delta.text.as_str(),
10449            // The lack indentation is a formatter bug.
10450            "\
10451sketch(on = XY) {
10452  point1 = point(at = [var 1, var 2])
10453  point2 = point(at = [var 3, var 4])
10454  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10455}
10456"
10457        );
10458        assert_eq!(
10459            scene_delta.new_graph.objects.len(),
10460            5,
10461            "{:#?}",
10462            scene_delta.new_graph.objects
10463        );
10464        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10465        let sketch = expect_sketch(sketch_object);
10466        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10467        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10468            panic!("Expected constraint object");
10469        };
10470        let Constraint::HorizontalDistance(distance) = constraint else {
10471            panic!("Expected horizontal distance constraint");
10472        };
10473        assert_eq!(distance.label_position, Some(label_position));
10474
10475        mock_ctx.close().await;
10476    }
10477
10478    #[tokio::test(flavor = "multi_thread")]
10479    async fn test_radius_single_arc_segment() {
10480        let initial_source = "\
10481sketch(on = XY) {
10482  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10483}
10484";
10485
10486        let program = Program::parse(initial_source).unwrap().0.unwrap();
10487
10488        let mut frontend = FrontendState::new();
10489
10490        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10491        let mock_ctx = ExecutorContext::new_mock(None).await;
10492        let version = Version(0);
10493
10494        frontend.hack_set_program(&ctx, program).await.unwrap();
10495        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10496        let sketch_id = sketch_object.id;
10497        let sketch = expect_sketch(sketch_object);
10498        // Find the arc segment (not the points)
10499        let arc_id = sketch
10500            .segments
10501            .iter()
10502            .find(|&seg_id| {
10503                let obj = frontend.scene_graph.objects.get(seg_id.0);
10504                matches!(
10505                    obj.map(|o| &o.kind),
10506                    Some(ObjectKind::Segment {
10507                        segment: Segment::Arc(_)
10508                    })
10509                )
10510            })
10511            .unwrap();
10512
10513        let constraint = Constraint::Radius(Radius {
10514            arc: *arc_id,
10515            radius: Number {
10516                value: 5.0,
10517                units: NumericSuffix::Mm,
10518            },
10519            label_position: None,
10520            source: Default::default(),
10521        });
10522        let (src_delta, scene_delta) = frontend
10523            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10524            .await
10525            .unwrap();
10526        assert_eq!(
10527            src_delta.text.as_str(),
10528            // The lack indentation is a formatter bug.
10529            "\
10530sketch(on = XY) {
10531  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10532  radius(arc1) == 5mm
10533}
10534"
10535        );
10536        assert_eq!(
10537            scene_delta.new_graph.objects.len(),
10538            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10539            "{:#?}",
10540            scene_delta.new_graph.objects
10541        );
10542
10543        ctx.close().await;
10544        mock_ctx.close().await;
10545    }
10546
10547    #[tokio::test(flavor = "multi_thread")]
10548    async fn test_radius_single_arc_segment_with_label_position() {
10549        let initial_source = "\
10550sketch(on = XY) {
10551  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10552}
10553";
10554
10555        let program = Program::parse(initial_source).unwrap().0.unwrap();
10556        let mut frontend = FrontendState::new();
10557        let mock_ctx = ExecutorContext::new_mock(None).await;
10558        let version = Version(0);
10559
10560        frontend.program = program.clone();
10561        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10562        frontend.update_state_after_exec(outcome, true);
10563        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10564        let sketch_id = sketch_object.id;
10565        let sketch = expect_sketch(sketch_object);
10566        let arc_id = sketch
10567            .segments
10568            .iter()
10569            .find(|&seg_id| {
10570                let obj = frontend.scene_graph.objects.get(seg_id.0);
10571                matches!(
10572                    obj.map(|o| &o.kind),
10573                    Some(ObjectKind::Segment {
10574                        segment: Segment::Arc(_)
10575                    })
10576                )
10577            })
10578            .unwrap();
10579
10580        let label_position = Point2d {
10581            x: Number {
10582                value: 10.0,
10583                units: NumericSuffix::Mm,
10584            },
10585            y: Number {
10586                value: 11.0,
10587                units: NumericSuffix::Mm,
10588            },
10589        };
10590        let constraint = Constraint::Radius(Radius {
10591            arc: *arc_id,
10592            radius: Number {
10593                value: 5.0,
10594                units: NumericSuffix::Mm,
10595            },
10596            label_position: Some(label_position.clone()),
10597            source: Default::default(),
10598        });
10599        let (src_delta, scene_delta) = frontend
10600            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10601            .await
10602            .unwrap();
10603        assert_eq!(
10604            src_delta.text.as_str(),
10605            "\
10606sketch(on = XY) {
10607  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10608  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10609}
10610"
10611        );
10612
10613        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10614        let sketch = expect_sketch(sketch_object);
10615        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10616        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10617            panic!("Expected constraint object");
10618        };
10619        let Constraint::Radius(radius) = constraint else {
10620            panic!("Expected radius constraint");
10621        };
10622        assert_eq!(radius.label_position, Some(label_position));
10623
10624        mock_ctx.close().await;
10625    }
10626
10627    #[tokio::test(flavor = "multi_thread")]
10628    async fn test_edit_radius_constraint_label_position() {
10629        let initial_source = "\
10630sketch(on = XY) {
10631  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10632  radius(arc1) == 5mm
10633}
10634";
10635
10636        let program = Program::parse(initial_source).unwrap().0.unwrap();
10637        let mut frontend = FrontendState::new();
10638        let mock_ctx = ExecutorContext::new_mock(None).await;
10639        let version = Version(0);
10640
10641        frontend.program = program.clone();
10642        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10643        frontend.update_state_after_exec(outcome, true);
10644        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10645        let sketch_id = sketch_object.id;
10646        let sketch = expect_sketch(sketch_object);
10647        let constraint_id = sketch.constraints[0];
10648        let label_position = Point2d {
10649            x: Number {
10650                value: 10.0,
10651                units: NumericSuffix::Mm,
10652            },
10653            y: Number {
10654                value: 11.0,
10655                units: NumericSuffix::Mm,
10656            },
10657        };
10658
10659        let (src_delta, scene_delta) = frontend
10660            .edit_distance_constraint_label_position(
10661                &mock_ctx,
10662                version,
10663                sketch_id,
10664                constraint_id,
10665                label_position.clone(),
10666                vec![],
10667            )
10668            .await
10669            .unwrap();
10670        assert_eq!(
10671            src_delta.text.as_str(),
10672            "\
10673sketch(on = XY) {
10674  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10675  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10676}
10677"
10678        );
10679
10680        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10681        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10682            panic!("Expected constraint object");
10683        };
10684        let Constraint::Radius(radius) = constraint else {
10685            panic!("Expected radius constraint");
10686        };
10687        assert_eq!(radius.label_position, Some(label_position));
10688
10689        mock_ctx.close().await;
10690    }
10691
10692    #[tokio::test(flavor = "multi_thread")]
10693    async fn test_vertical_distance_two_points() {
10694        let initial_source = "\
10695sketch(on = XY) {
10696  point(at = [var 1, var 2])
10697  point(at = [var 3, var 4])
10698}
10699";
10700
10701        let program = Program::parse(initial_source).unwrap().0.unwrap();
10702
10703        let mut frontend = FrontendState::new();
10704
10705        let mock_ctx = ExecutorContext::new_mock(None).await;
10706        let version = Version(0);
10707
10708        frontend.program = program.clone();
10709        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10710        frontend.update_state_after_exec(outcome, true);
10711        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10712        let sketch_id = sketch_object.id;
10713        let sketch = expect_sketch(sketch_object);
10714        let point0_id = *sketch.segments.first().unwrap();
10715        let point1_id = *sketch.segments.get(1).unwrap();
10716        let label_position = Point2d {
10717            x: Number {
10718                value: 10.0,
10719                units: NumericSuffix::Mm,
10720            },
10721            y: Number {
10722                value: 11.0,
10723                units: NumericSuffix::Mm,
10724            },
10725        };
10726
10727        let constraint = Constraint::VerticalDistance(Distance {
10728            points: vec![point0_id.into(), point1_id.into()],
10729            distance: Number {
10730                value: 2.0,
10731                units: NumericSuffix::Mm,
10732            },
10733            label_position: Some(label_position.clone()),
10734            source: Default::default(),
10735        });
10736        let (src_delta, scene_delta) = frontend
10737            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10738            .await
10739            .unwrap();
10740        assert_eq!(
10741            src_delta.text.as_str(),
10742            // The lack indentation is a formatter bug.
10743            "\
10744sketch(on = XY) {
10745  point1 = point(at = [var 1, var 2])
10746  point2 = point(at = [var 3, var 4])
10747  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10748}
10749"
10750        );
10751        assert_eq!(
10752            scene_delta.new_graph.objects.len(),
10753            5,
10754            "{:#?}",
10755            scene_delta.new_graph.objects
10756        );
10757        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10758        let sketch = expect_sketch(sketch_object);
10759        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10760        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10761            panic!("Expected constraint object");
10762        };
10763        let Constraint::VerticalDistance(distance) = constraint else {
10764            panic!("Expected vertical distance constraint");
10765        };
10766        assert_eq!(distance.label_position, Some(label_position));
10767
10768        mock_ctx.close().await;
10769    }
10770
10771    #[tokio::test(flavor = "multi_thread")]
10772    async fn test_add_fixed_standalone_point() {
10773        let initial_source = "\
10774sketch(on = XY) {
10775  point(at = [var 1, var 2])
10776}
10777";
10778
10779        let program = Program::parse(initial_source).unwrap().0.unwrap();
10780
10781        let mut frontend = FrontendState::new();
10782
10783        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10784        let mock_ctx = ExecutorContext::new_mock(None).await;
10785        let version = Version(0);
10786
10787        frontend.hack_set_program(&ctx, program).await.unwrap();
10788        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10789        let sketch_id = sketch_object.id;
10790        let sketch = expect_sketch(sketch_object);
10791        let point_id = *sketch.segments.first().unwrap();
10792
10793        let (src_delta, scene_delta) = frontend
10794            .add_constraint(
10795                &mock_ctx,
10796                version,
10797                sketch_id,
10798                Constraint::Fixed(Fixed {
10799                    points: vec![FixedPoint {
10800                        point: point_id,
10801                        position: Point2d {
10802                            x: Number {
10803                                value: 2.0,
10804                                units: NumericSuffix::Mm,
10805                            },
10806                            y: Number {
10807                                value: 3.0,
10808                                units: NumericSuffix::Mm,
10809                            },
10810                        },
10811                    }],
10812                }),
10813            )
10814            .await
10815            .unwrap();
10816        assert_eq!(
10817            src_delta.text.as_str(),
10818            "\
10819sketch(on = XY) {
10820  point1 = point(at = [var 1, var 2])
10821  fixed([point1, [2mm, 3mm]])
10822}
10823"
10824        );
10825        assert_eq!(
10826            scene_delta.new_graph.objects.len(),
10827            4,
10828            "{:#?}",
10829            scene_delta.new_graph.objects
10830        );
10831
10832        ctx.close().await;
10833        mock_ctx.close().await;
10834    }
10835
10836    #[tokio::test(flavor = "multi_thread")]
10837    async fn test_add_fixed_multiple_points() {
10838        let initial_source = "\
10839sketch(on = XY) {
10840  point(at = [var 1, var 2])
10841  point(at = [var 3, var 4])
10842}
10843";
10844
10845        let program = Program::parse(initial_source).unwrap().0.unwrap();
10846
10847        let mut frontend = FrontendState::new();
10848
10849        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10850        let mock_ctx = ExecutorContext::new_mock(None).await;
10851        let version = Version(0);
10852
10853        frontend.hack_set_program(&ctx, program).await.unwrap();
10854        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10855        let sketch_id = sketch_object.id;
10856        let sketch = expect_sketch(sketch_object);
10857        let point0_id = *sketch.segments.first().unwrap();
10858        let point1_id = *sketch.segments.get(1).unwrap();
10859
10860        let (src_delta, scene_delta) = frontend
10861            .add_constraint(
10862                &mock_ctx,
10863                version,
10864                sketch_id,
10865                Constraint::Fixed(Fixed {
10866                    points: vec![
10867                        FixedPoint {
10868                            point: point0_id,
10869                            position: Point2d {
10870                                x: Number {
10871                                    value: 2.0,
10872                                    units: NumericSuffix::Mm,
10873                                },
10874                                y: Number {
10875                                    value: 3.0,
10876                                    units: NumericSuffix::Mm,
10877                                },
10878                            },
10879                        },
10880                        FixedPoint {
10881                            point: point1_id,
10882                            position: Point2d {
10883                                x: Number {
10884                                    value: 4.0,
10885                                    units: NumericSuffix::Mm,
10886                                },
10887                                y: Number {
10888                                    value: 5.0,
10889                                    units: NumericSuffix::Mm,
10890                                },
10891                            },
10892                        },
10893                    ],
10894                }),
10895            )
10896            .await
10897            .unwrap();
10898        assert_eq!(
10899            src_delta.text.as_str(),
10900            "\
10901sketch(on = XY) {
10902  point1 = point(at = [var 1, var 2])
10903  point2 = point(at = [var 3, var 4])
10904  fixed([point1, [2mm, 3mm]])
10905  fixed([point2, [4mm, 5mm]])
10906}
10907"
10908        );
10909        assert_eq!(
10910            scene_delta.new_graph.objects.len(),
10911            6,
10912            "{:#?}",
10913            scene_delta.new_graph.objects
10914        );
10915
10916        ctx.close().await;
10917        mock_ctx.close().await;
10918    }
10919
10920    #[tokio::test(flavor = "multi_thread")]
10921    async fn test_add_fixed_owned_point() {
10922        let initial_source = "\
10923sketch(on = XY) {
10924  line(start = [var 1, var 2], end = [var 3, var 4])
10925}
10926";
10927
10928        let program = Program::parse(initial_source).unwrap().0.unwrap();
10929
10930        let mut frontend = FrontendState::new();
10931
10932        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10933        let mock_ctx = ExecutorContext::new_mock(None).await;
10934        let version = Version(0);
10935
10936        frontend.hack_set_program(&ctx, program).await.unwrap();
10937        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10938        let sketch_id = sketch_object.id;
10939        let sketch = expect_sketch(sketch_object);
10940        let line_start_id = *sketch.segments.first().unwrap();
10941
10942        let (src_delta, scene_delta) = frontend
10943            .add_constraint(
10944                &mock_ctx,
10945                version,
10946                sketch_id,
10947                Constraint::Fixed(Fixed {
10948                    points: vec![FixedPoint {
10949                        point: line_start_id,
10950                        position: Point2d {
10951                            x: Number {
10952                                value: 2.0,
10953                                units: NumericSuffix::Mm,
10954                            },
10955                            y: Number {
10956                                value: 3.0,
10957                                units: NumericSuffix::Mm,
10958                            },
10959                        },
10960                    }],
10961                }),
10962            )
10963            .await
10964            .unwrap();
10965        assert_eq!(
10966            src_delta.text.as_str(),
10967            "\
10968sketch(on = XY) {
10969  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10970  fixed([line1.start, [2mm, 3mm]])
10971}
10972"
10973        );
10974        assert_eq!(
10975            scene_delta.new_graph.objects.len(),
10976            6,
10977            "{:#?}",
10978            scene_delta.new_graph.objects
10979        );
10980
10981        ctx.close().await;
10982        mock_ctx.close().await;
10983    }
10984
10985    #[tokio::test(flavor = "multi_thread")]
10986    async fn test_radius_error_cases() {
10987        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10988        let mock_ctx = ExecutorContext::new_mock(None).await;
10989        let version = Version(0);
10990
10991        // Test: Single point should error
10992        let initial_source_point = "\
10993sketch(on = XY) {
10994  point(at = [var 1, var 2])
10995}
10996";
10997        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10998        let mut frontend_point = FrontendState::new();
10999        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11000        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11001        let sketch_id_point = sketch_object_point.id;
11002        let sketch_point = expect_sketch(sketch_object_point);
11003        let point_id = *sketch_point.segments.first().unwrap();
11004
11005        let constraint_point = Constraint::Radius(Radius {
11006            arc: point_id,
11007            radius: Number {
11008                value: 5.0,
11009                units: NumericSuffix::Mm,
11010            },
11011            label_position: None,
11012            source: Default::default(),
11013        });
11014        let result_point = frontend_point
11015            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11016            .await;
11017        assert!(result_point.is_err(), "Single point should error for radius");
11018
11019        // Test: Single line segment should error (only arc segments supported)
11020        let initial_source_line = "\
11021sketch(on = XY) {
11022  line(start = [var 1, var 2], end = [var 3, var 4])
11023}
11024";
11025        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11026        let mut frontend_line = FrontendState::new();
11027        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11028        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11029        let sketch_id_line = sketch_object_line.id;
11030        let sketch_line = expect_sketch(sketch_object_line);
11031        let line_id = *sketch_line.segments.first().unwrap();
11032
11033        let constraint_line = Constraint::Radius(Radius {
11034            arc: line_id,
11035            radius: Number {
11036                value: 5.0,
11037                units: NumericSuffix::Mm,
11038            },
11039            label_position: None,
11040            source: Default::default(),
11041        });
11042        let result_line = frontend_line
11043            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11044            .await;
11045        assert!(result_line.is_err(), "Single line segment should error for radius");
11046
11047        ctx.close().await;
11048        mock_ctx.close().await;
11049    }
11050
11051    #[tokio::test(flavor = "multi_thread")]
11052    async fn test_diameter_single_arc_segment() {
11053        let initial_source = "\
11054sketch(on = XY) {
11055  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11056}
11057";
11058
11059        let program = Program::parse(initial_source).unwrap().0.unwrap();
11060
11061        let mut frontend = FrontendState::new();
11062
11063        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11064        let mock_ctx = ExecutorContext::new_mock(None).await;
11065        let version = Version(0);
11066
11067        frontend.hack_set_program(&ctx, program).await.unwrap();
11068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11069        let sketch_id = sketch_object.id;
11070        let sketch = expect_sketch(sketch_object);
11071        // Find the arc segment (not the points)
11072        let arc_id = sketch
11073            .segments
11074            .iter()
11075            .find(|&seg_id| {
11076                let obj = frontend.scene_graph.objects.get(seg_id.0);
11077                matches!(
11078                    obj.map(|o| &o.kind),
11079                    Some(ObjectKind::Segment {
11080                        segment: Segment::Arc(_)
11081                    })
11082                )
11083            })
11084            .unwrap();
11085
11086        let constraint = Constraint::Diameter(Diameter {
11087            arc: *arc_id,
11088            diameter: Number {
11089                value: 10.0,
11090                units: NumericSuffix::Mm,
11091            },
11092            label_position: None,
11093            source: Default::default(),
11094        });
11095        let (src_delta, scene_delta) = frontend
11096            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11097            .await
11098            .unwrap();
11099        assert_eq!(
11100            src_delta.text.as_str(),
11101            // The lack indentation is a formatter bug.
11102            "\
11103sketch(on = XY) {
11104  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11105  diameter(arc1) == 10mm
11106}
11107"
11108        );
11109        assert_eq!(
11110            scene_delta.new_graph.objects.len(),
11111            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11112            "{:#?}",
11113            scene_delta.new_graph.objects
11114        );
11115
11116        ctx.close().await;
11117        mock_ctx.close().await;
11118    }
11119
11120    #[tokio::test(flavor = "multi_thread")]
11121    async fn test_diameter_single_arc_segment_with_label_position() {
11122        let initial_source = "\
11123sketch(on = XY) {
11124  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11125}
11126";
11127
11128        let program = Program::parse(initial_source).unwrap().0.unwrap();
11129        let mut frontend = FrontendState::new();
11130        let mock_ctx = ExecutorContext::new_mock(None).await;
11131        let version = Version(0);
11132
11133        frontend.program = program.clone();
11134        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11135        frontend.update_state_after_exec(outcome, true);
11136        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11137        let sketch_id = sketch_object.id;
11138        let sketch = expect_sketch(sketch_object);
11139        let arc_id = sketch
11140            .segments
11141            .iter()
11142            .find(|&seg_id| {
11143                let obj = frontend.scene_graph.objects.get(seg_id.0);
11144                matches!(
11145                    obj.map(|o| &o.kind),
11146                    Some(ObjectKind::Segment {
11147                        segment: Segment::Arc(_)
11148                    })
11149                )
11150            })
11151            .unwrap();
11152
11153        let label_position = Point2d {
11154            x: Number {
11155                value: 10.0,
11156                units: NumericSuffix::Mm,
11157            },
11158            y: Number {
11159                value: 11.0,
11160                units: NumericSuffix::Mm,
11161            },
11162        };
11163        let constraint = Constraint::Diameter(Diameter {
11164            arc: *arc_id,
11165            diameter: Number {
11166                value: 10.0,
11167                units: NumericSuffix::Mm,
11168            },
11169            label_position: Some(label_position.clone()),
11170            source: Default::default(),
11171        });
11172        let (src_delta, scene_delta) = frontend
11173            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11174            .await
11175            .unwrap();
11176        assert_eq!(
11177            src_delta.text.as_str(),
11178            "\
11179sketch(on = XY) {
11180  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11181  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11182}
11183"
11184        );
11185
11186        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11187        let sketch = expect_sketch(sketch_object);
11188        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11189        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11190            panic!("Expected constraint object");
11191        };
11192        let Constraint::Diameter(diameter) = constraint else {
11193            panic!("Expected diameter constraint");
11194        };
11195        assert_eq!(diameter.label_position, Some(label_position));
11196
11197        mock_ctx.close().await;
11198    }
11199
11200    #[tokio::test(flavor = "multi_thread")]
11201    async fn test_edit_diameter_constraint_label_position() {
11202        let initial_source = "\
11203sketch(on = XY) {
11204  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11205  diameter(arc1) == 10mm
11206}
11207";
11208
11209        let program = Program::parse(initial_source).unwrap().0.unwrap();
11210        let mut frontend = FrontendState::new();
11211        let mock_ctx = ExecutorContext::new_mock(None).await;
11212        let version = Version(0);
11213
11214        frontend.program = program.clone();
11215        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11216        frontend.update_state_after_exec(outcome, true);
11217        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11218        let sketch_id = sketch_object.id;
11219        let sketch = expect_sketch(sketch_object);
11220        let constraint_id = sketch.constraints[0];
11221        let label_position = Point2d {
11222            x: Number {
11223                value: 10.0,
11224                units: NumericSuffix::Mm,
11225            },
11226            y: Number {
11227                value: 11.0,
11228                units: NumericSuffix::Mm,
11229            },
11230        };
11231
11232        let (src_delta, scene_delta) = frontend
11233            .edit_distance_constraint_label_position(
11234                &mock_ctx,
11235                version,
11236                sketch_id,
11237                constraint_id,
11238                label_position.clone(),
11239                vec![],
11240            )
11241            .await
11242            .unwrap();
11243        assert_eq!(
11244            src_delta.text.as_str(),
11245            "\
11246sketch(on = XY) {
11247  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11248  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11249}
11250"
11251        );
11252
11253        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11254        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11255            panic!("Expected constraint object");
11256        };
11257        let Constraint::Diameter(diameter) = constraint else {
11258            panic!("Expected diameter constraint");
11259        };
11260        assert_eq!(diameter.label_position, Some(label_position));
11261
11262        mock_ctx.close().await;
11263    }
11264
11265    #[tokio::test(flavor = "multi_thread")]
11266    async fn test_diameter_error_cases() {
11267        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11268        let mock_ctx = ExecutorContext::new_mock(None).await;
11269        let version = Version(0);
11270
11271        // Test: Single point should error
11272        let initial_source_point = "\
11273sketch(on = XY) {
11274  point(at = [var 1, var 2])
11275}
11276";
11277        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11278        let mut frontend_point = FrontendState::new();
11279        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11280        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11281        let sketch_id_point = sketch_object_point.id;
11282        let sketch_point = expect_sketch(sketch_object_point);
11283        let point_id = *sketch_point.segments.first().unwrap();
11284
11285        let constraint_point = Constraint::Diameter(Diameter {
11286            arc: point_id,
11287            diameter: Number {
11288                value: 10.0,
11289                units: NumericSuffix::Mm,
11290            },
11291            label_position: None,
11292            source: Default::default(),
11293        });
11294        let result_point = frontend_point
11295            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11296            .await;
11297        assert!(result_point.is_err(), "Single point should error for diameter");
11298
11299        // Test: Single line segment should error (only arc segments supported)
11300        let initial_source_line = "\
11301sketch(on = XY) {
11302  line(start = [var 1, var 2], end = [var 3, var 4])
11303}
11304";
11305        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11306        let mut frontend_line = FrontendState::new();
11307        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11308        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11309        let sketch_id_line = sketch_object_line.id;
11310        let sketch_line = expect_sketch(sketch_object_line);
11311        let line_id = *sketch_line.segments.first().unwrap();
11312
11313        let constraint_line = Constraint::Diameter(Diameter {
11314            arc: line_id,
11315            diameter: Number {
11316                value: 10.0,
11317                units: NumericSuffix::Mm,
11318            },
11319            label_position: None,
11320            source: Default::default(),
11321        });
11322        let result_line = frontend_line
11323            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11324            .await;
11325        assert!(result_line.is_err(), "Single line segment should error for diameter");
11326
11327        ctx.close().await;
11328        mock_ctx.close().await;
11329    }
11330
11331    #[tokio::test(flavor = "multi_thread")]
11332    async fn test_line_horizontal() {
11333        let initial_source = "\
11334sketch(on = XY) {
11335  line(start = [var 1, var 2], end = [var 3, var 4])
11336}
11337";
11338
11339        let program = Program::parse(initial_source).unwrap().0.unwrap();
11340
11341        let mut frontend = FrontendState::new();
11342
11343        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11344        let mock_ctx = ExecutorContext::new_mock(None).await;
11345        let version = Version(0);
11346
11347        frontend.hack_set_program(&ctx, program).await.unwrap();
11348        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11349        let sketch_id = sketch_object.id;
11350        let sketch = expect_sketch(sketch_object);
11351        let line1_id = *sketch.segments.get(2).unwrap();
11352
11353        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
11354        let (src_delta, scene_delta) = frontend
11355            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11356            .await
11357            .unwrap();
11358        assert_eq!(
11359            src_delta.text.as_str(),
11360            "\
11361sketch(on = XY) {
11362  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11363  horizontal(line1)
11364}
11365"
11366        );
11367        assert_eq!(
11368            scene_delta.new_graph.objects.len(),
11369            6,
11370            "{:#?}",
11371            scene_delta.new_graph.objects
11372        );
11373
11374        ctx.close().await;
11375        mock_ctx.close().await;
11376    }
11377
11378    #[tokio::test(flavor = "multi_thread")]
11379    async fn test_control_point_spline_edge_horizontal() {
11380        let initial_source = "\
11381@settings(experimentalFeatures = allow)
11382splineSketch = sketch(on = XY) {
11383  controlPointSpline1 = controlPointSpline(points = [
11384    [var 0mm, var 0mm],
11385    [var 10mm, var 20mm],
11386    [var 20mm, var 0mm],
11387  ])
11388}
11389";
11390
11391        let program = Program::parse(initial_source).unwrap().0.unwrap();
11392
11393        let mut frontend = FrontendState::new();
11394
11395        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11396        let mock_ctx = ExecutorContext::new_mock(None).await;
11397        let version = Version(0);
11398
11399        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11400        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11401        let sketch_id = sketch_object.id;
11402        let sketch = expect_sketch(sketch_object);
11403        let spline_id = sketch
11404            .segments
11405            .iter()
11406            .copied()
11407            .find(|seg_id| {
11408                matches!(
11409                    &frontend.scene_graph.objects[seg_id.0].kind,
11410                    ObjectKind::Segment {
11411                        segment: Segment::ControlPointSpline(_)
11412                    }
11413                )
11414            })
11415            .expect("Expected a control point spline segment in sketch");
11416        let edge_id = frontend
11417            .scene_graph
11418            .objects
11419            .iter()
11420            .find_map(|obj| match &obj.kind {
11421                ObjectKind::Segment {
11422                    segment: Segment::Line(line),
11423                } if line.owner == Some(spline_id) => Some(obj.id),
11424                _ => None,
11425            })
11426            .expect("Expected an owned control-polygon edge");
11427
11428        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
11429        let (src_delta, _) = frontend
11430            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11431            .await
11432            .unwrap();
11433        assert!(
11434            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
11435            "Expected horizontal constraint on spline edge, got: {}",
11436            src_delta.text
11437        );
11438
11439        ctx.close().await;
11440        mock_ctx.close().await;
11441    }
11442
11443    #[tokio::test(flavor = "multi_thread")]
11444    async fn test_control_point_spline_edge_angle() {
11445        let initial_source = "\
11446@settings(experimentalFeatures = allow)
11447splineSketch = sketch(on = XY) {
11448  controlPointSpline1 = controlPointSpline(points = [
11449    [var 0mm, var 0mm],
11450    [var 10mm, var 20mm],
11451    [var 20mm, var 0mm],
11452  ])
11453
11454  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
11455}
11456";
11457
11458        let program = Program::parse(initial_source).unwrap().0.unwrap();
11459
11460        let mut frontend = FrontendState::new();
11461
11462        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11463        let mock_ctx = ExecutorContext::new_mock(None).await;
11464        let version = Version(0);
11465
11466        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11468        let sketch_id = sketch_object.id;
11469        let sketch = expect_sketch(sketch_object);
11470        let spline_id = sketch
11471            .segments
11472            .iter()
11473            .copied()
11474            .find(|seg_id| {
11475                matches!(
11476                    &frontend.scene_graph.objects[seg_id.0].kind,
11477                    ObjectKind::Segment {
11478                        segment: Segment::ControlPointSpline(_)
11479                    }
11480                )
11481            })
11482            .expect("Expected a control point spline segment in sketch");
11483        let edge_id = frontend
11484            .scene_graph
11485            .objects
11486            .iter()
11487            .find_map(|obj| match &obj.kind {
11488                ObjectKind::Segment {
11489                    segment: Segment::Line(line),
11490                } if line.owner == Some(spline_id) => Some(obj.id),
11491                _ => None,
11492            })
11493            .expect("Expected an owned control-polygon edge");
11494        let line1_id = frontend
11495            .scene_graph
11496            .objects
11497            .iter()
11498            .find_map(|obj| match &obj.kind {
11499                ObjectKind::Segment {
11500                    segment: Segment::Line(line),
11501                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
11502                _ => None,
11503            })
11504            .or_else(|| {
11505                sketch.segments.iter().copied().find(|seg_id| {
11506                    matches!(
11507                        &frontend.scene_graph.objects[seg_id.0].kind,
11508                        ObjectKind::Segment {
11509                            segment: Segment::Line(line),
11510                        } if line.owner.is_none()
11511                    )
11512                })
11513            })
11514            .expect("Expected a standalone line segment in sketch");
11515
11516        let constraint = Constraint::Angle(Angle {
11517            lines: vec![line1_id, edge_id],
11518            angle: Number {
11519                value: 30.0,
11520                units: NumericSuffix::Deg,
11521            },
11522            source: Default::default(),
11523        });
11524        let (src_delta, _) = frontend
11525            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11526            .await
11527            .unwrap();
11528        assert!(
11529            src_delta
11530                .text
11531                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
11532            "Expected angle constraint on spline edge, got: {}",
11533            src_delta.text
11534        );
11535
11536        ctx.close().await;
11537        mock_ctx.close().await;
11538    }
11539
11540    #[tokio::test(flavor = "multi_thread")]
11541    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
11542        let initial_source = "\
11543@settings(experimentalFeatures = allow)
11544splineSketch = sketch(on = XY) {
11545  spline1 = controlPointSpline(points = [
11546    [var 0mm, var 0mm],
11547    [var 10mm, var 20mm],
11548    [var 20mm, var 0mm],
11549  ])
11550  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
11551  coincident([spline1.controls[1], spline1.edges[0]])
11552  coincident([spline1.controls[0], line1])
11553}
11554";
11555
11556        let program = Program::parse(initial_source).unwrap().0.unwrap();
11557
11558        let mut frontend = FrontendState::new();
11559
11560        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11561        let mock_ctx = ExecutorContext::new_mock(None).await;
11562
11563        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11564
11565        let ui_scene_graph = frontend.scene_graph_for_ui();
11566        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
11567        let sketch = expect_sketch(sketch_object);
11568
11569        assert_eq!(
11570            sketch.constraints.len(),
11571            1,
11572            "Expected only the external coincident constraint to remain visible in the UI scene graph"
11573        );
11574
11575        let visible_constraints = ui_scene_graph
11576            .objects
11577            .iter()
11578            .filter_map(|object| match &object.kind {
11579                ObjectKind::Constraint {
11580                    constraint: Constraint::Coincident(coincident),
11581                } => Some(coincident.clone()),
11582                _ => None,
11583            })
11584            .collect::<Vec<_>>();
11585
11586        assert_eq!(
11587            visible_constraints.len(),
11588            1,
11589            "Expected only one coincident constraint object in the UI scene graph"
11590        );
11591        assert_eq!(
11592            visible_constraints[0].get_segments().len(),
11593            2,
11594            "Expected the remaining visible coincident constraint to reference two segments"
11595        );
11596
11597        ctx.close().await;
11598        mock_ctx.close().await;
11599    }
11600
11601    #[tokio::test(flavor = "multi_thread")]
11602    async fn test_edit_control_point_spline_can_append_control_point() {
11603        let initial_source = "\
11604@settings(experimentalFeatures = allow)
11605splineSketch = sketch(on = XY) {
11606  controlPointSpline(points = [
11607    [var 0mm, var 0mm],
11608    [var 10mm, var 20mm],
11609    [var 20mm, var 0mm],
11610  ])
11611}
11612";
11613
11614        let program = Program::parse(initial_source).unwrap().0.unwrap();
11615
11616        let mut frontend = FrontendState::new();
11617
11618        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11619        let mock_ctx = ExecutorContext::new_mock(None).await;
11620        let version = Version(0);
11621
11622        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11623        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11624        let sketch_id = sketch_object.id;
11625        let sketch = expect_sketch(sketch_object);
11626        let spline_id = sketch
11627            .segments
11628            .iter()
11629            .copied()
11630            .find(|seg_id| {
11631                matches!(
11632                    &frontend.scene_graph.objects[seg_id.0].kind,
11633                    ObjectKind::Segment {
11634                        segment: Segment::ControlPointSpline(_)
11635                    }
11636                )
11637            })
11638            .expect("Expected a control point spline segment in sketch");
11639
11640        let ctor = ControlPointSplineCtor {
11641            points: vec![
11642                Point2d {
11643                    x: Expr::Var(Number {
11644                        value: 0.0,
11645                        units: NumericSuffix::Mm,
11646                    }),
11647                    y: Expr::Var(Number {
11648                        value: 0.0,
11649                        units: NumericSuffix::Mm,
11650                    }),
11651                },
11652                Point2d {
11653                    x: Expr::Var(Number {
11654                        value: 10.0,
11655                        units: NumericSuffix::Mm,
11656                    }),
11657                    y: Expr::Var(Number {
11658                        value: 20.0,
11659                        units: NumericSuffix::Mm,
11660                    }),
11661                },
11662                Point2d {
11663                    x: Expr::Var(Number {
11664                        value: 20.0,
11665                        units: NumericSuffix::Mm,
11666                    }),
11667                    y: Expr::Var(Number {
11668                        value: 0.0,
11669                        units: NumericSuffix::Mm,
11670                    }),
11671                },
11672                Point2d {
11673                    x: Expr::Var(Number {
11674                        value: 30.0,
11675                        units: NumericSuffix::Mm,
11676                    }),
11677                    y: Expr::Var(Number {
11678                        value: 10.0,
11679                        units: NumericSuffix::Mm,
11680                    }),
11681                },
11682            ],
11683            construction: None,
11684        };
11685
11686        let segments = vec![ExistingSegmentCtor {
11687            id: spline_id,
11688            ctor: SegmentCtor::ControlPointSpline(ctor),
11689        }];
11690        let (src_delta, scene_delta) = frontend
11691            .edit_segments(&mock_ctx, version, sketch_id, segments)
11692            .await
11693            .unwrap();
11694
11695        assert!(
11696            src_delta.text.contains("[var 30mm, var 10mm]"),
11697            "Expected appended spline control point in source, got: {}",
11698            src_delta.text
11699        );
11700
11701        assert!(
11702            scene_delta.invalidates_ids,
11703            "Expected appending a spline control point to invalidate ids"
11704        );
11705        let updated_spline = scene_delta
11706            .new_graph
11707            .objects
11708            .iter()
11709            .find_map(|obj| match &obj.kind {
11710                ObjectKind::Segment {
11711                    segment: Segment::ControlPointSpline(updated_spline),
11712                } if updated_spline.controls.len() == 4 => Some(updated_spline),
11713                _ => None,
11714            })
11715            .expect("Expected edited scene graph to contain a four-point control point spline");
11716        assert_eq!(
11717            updated_spline.controls.len(),
11718            4,
11719            "Expected edited spline to expose four control points"
11720        );
11721
11722        ctx.close().await;
11723        mock_ctx.close().await;
11724    }
11725
11726    #[tokio::test(flavor = "multi_thread")]
11727    async fn test_line_vertical() {
11728        let initial_source = "\
11729sketch(on = XY) {
11730  line(start = [var 1, var 2], end = [var 3, var 4])
11731}
11732";
11733
11734        let program = Program::parse(initial_source).unwrap().0.unwrap();
11735
11736        let mut frontend = FrontendState::new();
11737
11738        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11739        let mock_ctx = ExecutorContext::new_mock(None).await;
11740        let version = Version(0);
11741
11742        frontend.hack_set_program(&ctx, program).await.unwrap();
11743        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11744        let sketch_id = sketch_object.id;
11745        let sketch = expect_sketch(sketch_object);
11746        let line1_id = *sketch.segments.get(2).unwrap();
11747
11748        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
11749        let (src_delta, scene_delta) = frontend
11750            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11751            .await
11752            .unwrap();
11753        assert_eq!(
11754            src_delta.text.as_str(),
11755            "\
11756sketch(on = XY) {
11757  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11758  vertical(line1)
11759}
11760"
11761        );
11762        assert_eq!(
11763            scene_delta.new_graph.objects.len(),
11764            6,
11765            "{:#?}",
11766            scene_delta.new_graph.objects
11767        );
11768
11769        ctx.close().await;
11770        mock_ctx.close().await;
11771    }
11772
11773    #[tokio::test(flavor = "multi_thread")]
11774    async fn test_points_vertical() {
11775        let initial_source = "\
11776sketch001 = sketch(on = XY) {
11777  p0 = point(at = [var -2.23mm, var 3.1mm])
11778  pf = point(at = [4, 4])
11779}
11780";
11781
11782        let program = Program::parse(initial_source).unwrap().0.unwrap();
11783
11784        let mut frontend = FrontendState::new();
11785
11786        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11787        let mock_ctx = ExecutorContext::new_mock(None).await;
11788        let version = Version(0);
11789
11790        frontend.hack_set_program(&ctx, program).await.unwrap();
11791        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11792        let sketch_id = sketch_object.id;
11793        let sketch = expect_sketch(sketch_object);
11794        let point_ids = vec![
11795            sketch.segments.first().unwrap().to_owned(),
11796            sketch.segments.get(1).unwrap().to_owned(),
11797        ];
11798
11799        let constraint = Constraint::Vertical(Vertical::Points {
11800            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11801        });
11802        let (src_delta, scene_delta) = frontend
11803            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11804            .await
11805            .unwrap();
11806        assert_eq!(
11807            src_delta.text.as_str(),
11808            "\
11809sketch001 = sketch(on = XY) {
11810  p0 = point(at = [var -2.23mm, var 3.1mm])
11811  pf = point(at = [4, 4])
11812  vertical([p0, pf])
11813}
11814"
11815        );
11816        assert_eq!(
11817            scene_delta.new_graph.objects.len(),
11818            5,
11819            "{:#?}",
11820            scene_delta.new_graph.objects
11821        );
11822
11823        ctx.close().await;
11824        mock_ctx.close().await;
11825    }
11826
11827    #[tokio::test(flavor = "multi_thread")]
11828    async fn test_points_horizontal() {
11829        let initial_source = "\
11830sketch001 = sketch(on = XY) {
11831  p0 = point(at = [var -2.23mm, var 3.1mm])
11832  pf = point(at = [4, 4])
11833}
11834";
11835
11836        let program = Program::parse(initial_source).unwrap().0.unwrap();
11837
11838        let mut frontend = FrontendState::new();
11839
11840        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11841        let mock_ctx = ExecutorContext::new_mock(None).await;
11842        let version = Version(0);
11843
11844        frontend.hack_set_program(&ctx, program).await.unwrap();
11845        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11846        let sketch_id = sketch_object.id;
11847        let sketch = expect_sketch(sketch_object);
11848        let point_ids = vec![
11849            sketch.segments.first().unwrap().to_owned(),
11850            sketch.segments.get(1).unwrap().to_owned(),
11851        ];
11852
11853        let constraint = Constraint::Horizontal(Horizontal::Points {
11854            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11855        });
11856        let (src_delta, scene_delta) = frontend
11857            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11858            .await
11859            .unwrap();
11860        assert_eq!(
11861            src_delta.text.as_str(),
11862            "\
11863sketch001 = sketch(on = XY) {
11864  p0 = point(at = [var -2.23mm, var 3.1mm])
11865  pf = point(at = [4, 4])
11866  horizontal([p0, pf])
11867}
11868"
11869        );
11870        assert_eq!(
11871            scene_delta.new_graph.objects.len(),
11872            5,
11873            "{:#?}",
11874            scene_delta.new_graph.objects
11875        );
11876
11877        ctx.close().await;
11878        mock_ctx.close().await;
11879    }
11880
11881    #[tokio::test(flavor = "multi_thread")]
11882    async fn test_point_horizontal_with_origin() {
11883        let initial_source = "\
11884sketch001 = sketch(on = XY) {
11885  p0 = point(at = [var -2.23mm, var 3.1mm])
11886}
11887";
11888
11889        let program = Program::parse(initial_source).unwrap().0.unwrap();
11890
11891        let mut frontend = FrontendState::new();
11892
11893        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11894        let mock_ctx = ExecutorContext::new_mock(None).await;
11895        let version = Version(0);
11896
11897        frontend.hack_set_program(&ctx, program).await.unwrap();
11898        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11899        let sketch_id = sketch_object.id;
11900        let sketch = expect_sketch(sketch_object);
11901        let point_id = *sketch.segments.first().unwrap();
11902
11903        let constraint = Constraint::Horizontal(Horizontal::Points {
11904            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11905        });
11906        let (src_delta, scene_delta) = frontend
11907            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11908            .await
11909            .unwrap();
11910        assert_eq!(
11911            src_delta.text.as_str(),
11912            "\
11913sketch001 = sketch(on = XY) {
11914  p0 = point(at = [var -2.23mm, var 3.1mm])
11915  horizontal([p0, ORIGIN])
11916}
11917"
11918        );
11919        assert_eq!(
11920            scene_delta.new_graph.objects.len(),
11921            4,
11922            "{:#?}",
11923            scene_delta.new_graph.objects
11924        );
11925
11926        ctx.close().await;
11927        mock_ctx.close().await;
11928    }
11929
11930    #[tokio::test(flavor = "multi_thread")]
11931    async fn test_lines_equal_length() {
11932        let initial_source = "\
11933sketch(on = XY) {
11934  line(start = [var 1, var 2], end = [var 3, var 4])
11935  line(start = [var 5, var 6], end = [var 7, var 8])
11936}
11937";
11938
11939        let program = Program::parse(initial_source).unwrap().0.unwrap();
11940
11941        let mut frontend = FrontendState::new();
11942
11943        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11944        let mock_ctx = ExecutorContext::new_mock(None).await;
11945        let version = Version(0);
11946
11947        frontend.hack_set_program(&ctx, program).await.unwrap();
11948        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11949        let sketch_id = sketch_object.id;
11950        let sketch = expect_sketch(sketch_object);
11951        let line1_id = *sketch.segments.get(2).unwrap();
11952        let line2_id = *sketch.segments.get(5).unwrap();
11953
11954        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11955            lines: vec![line1_id, line2_id],
11956        });
11957        let (src_delta, scene_delta) = frontend
11958            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11959            .await
11960            .unwrap();
11961        assert_eq!(
11962            src_delta.text.as_str(),
11963            "\
11964sketch(on = XY) {
11965  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11966  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11967  equalLength([line1, line2])
11968}
11969"
11970        );
11971        assert_eq!(
11972            scene_delta.new_graph.objects.len(),
11973            9,
11974            "{:#?}",
11975            scene_delta.new_graph.objects
11976        );
11977
11978        ctx.close().await;
11979        mock_ctx.close().await;
11980    }
11981
11982    #[tokio::test(flavor = "multi_thread")]
11983    async fn test_add_constraint_multi_line_equal_length() {
11984        let initial_source = "\
11985sketch(on = XY) {
11986  line(start = [var 1, var 2], end = [var 3, var 4])
11987  line(start = [var 5, var 6], end = [var 7, var 8])
11988  line(start = [var 9, var 10], end = [var 11, var 12])
11989}
11990";
11991
11992        let program = Program::parse(initial_source).unwrap().0.unwrap();
11993
11994        let mut frontend = FrontendState::new();
11995        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11996        let mock_ctx = ExecutorContext::new_mock(None).await;
11997        let version = Version(0);
11998
11999        frontend.hack_set_program(&ctx, program).await.unwrap();
12000        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12001        let sketch_id = sketch_object.id;
12002        let sketch = expect_sketch(sketch_object);
12003        let line1_id = *sketch.segments.get(2).unwrap();
12004        let line2_id = *sketch.segments.get(5).unwrap();
12005        let line3_id = *sketch.segments.get(8).unwrap();
12006
12007        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12008            lines: vec![line1_id, line2_id, line3_id],
12009        });
12010        let (src_delta, scene_delta) = frontend
12011            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12012            .await
12013            .unwrap();
12014        assert_eq!(
12015            src_delta.text.as_str(),
12016            "\
12017sketch(on = XY) {
12018  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12019  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12020  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12021  equalLength([line1, line2, line3])
12022}
12023"
12024        );
12025        let constraints = scene_delta
12026            .new_graph
12027            .objects
12028            .iter()
12029            .filter_map(|obj| {
12030                let ObjectKind::Constraint { constraint } = &obj.kind else {
12031                    return None;
12032                };
12033                Some(constraint)
12034            })
12035            .collect::<Vec<_>>();
12036
12037        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12038        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12039            panic!("expected equal length constraint, got {:?}", constraints[0]);
12040        };
12041        assert_eq!(lines_equal_length.lines.len(), 3);
12042
12043        ctx.close().await;
12044        mock_ctx.close().await;
12045    }
12046
12047    #[tokio::test(flavor = "multi_thread")]
12048    async fn test_lines_parallel() {
12049        let initial_source = "\
12050sketch(on = XY) {
12051  line(start = [var 1, var 2], end = [var 3, var 4])
12052  line(start = [var 5, var 6], end = [var 7, var 8])
12053}
12054";
12055
12056        let program = Program::parse(initial_source).unwrap().0.unwrap();
12057
12058        let mut frontend = FrontendState::new();
12059
12060        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12061        let mock_ctx = ExecutorContext::new_mock(None).await;
12062        let version = Version(0);
12063
12064        frontend.hack_set_program(&ctx, program).await.unwrap();
12065        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12066        let sketch_id = sketch_object.id;
12067        let sketch = expect_sketch(sketch_object);
12068        let line1_id = *sketch.segments.get(2).unwrap();
12069        let line2_id = *sketch.segments.get(5).unwrap();
12070
12071        let constraint = Constraint::Parallel(Parallel {
12072            lines: vec![line1_id, line2_id],
12073        });
12074        let (src_delta, scene_delta) = frontend
12075            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12076            .await
12077            .unwrap();
12078        assert_eq!(
12079            src_delta.text.as_str(),
12080            "\
12081sketch(on = XY) {
12082  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12083  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12084  parallel([line1, line2])
12085}
12086"
12087        );
12088        assert_eq!(
12089            scene_delta.new_graph.objects.len(),
12090            9,
12091            "{:#?}",
12092            scene_delta.new_graph.objects
12093        );
12094
12095        ctx.close().await;
12096        mock_ctx.close().await;
12097    }
12098
12099    #[tokio::test(flavor = "multi_thread")]
12100    async fn test_lines_parallel_multiline() {
12101        let initial_source = "\
12102sketch(on = XY) {
12103  line(start = [var 1, var 2], end = [var 3, var 4])
12104  line(start = [var 5, var 6], end = [var 7, var 8])
12105  line(start = [var 9, var 10], end = [var 11, var 12])
12106}
12107";
12108
12109        let program = Program::parse(initial_source).unwrap().0.unwrap();
12110
12111        let mut frontend = FrontendState::new();
12112
12113        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12114        let mock_ctx = ExecutorContext::new_mock(None).await;
12115        let version = Version(0);
12116
12117        frontend.hack_set_program(&ctx, program).await.unwrap();
12118        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12119        let sketch_id = sketch_object.id;
12120        let sketch = expect_sketch(sketch_object);
12121        let line1_id = *sketch.segments.get(2).unwrap();
12122        let line2_id = *sketch.segments.get(5).unwrap();
12123        let line3_id = *sketch.segments.get(8).unwrap();
12124
12125        let constraint = Constraint::Parallel(Parallel {
12126            lines: vec![line1_id, line2_id, line3_id],
12127        });
12128        let (src_delta, scene_delta) = frontend
12129            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12130            .await
12131            .unwrap();
12132        assert_eq!(
12133            src_delta.text.as_str(),
12134            "\
12135sketch(on = XY) {
12136  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12137  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12138  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12139  parallel([line1, line2, line3])
12140}
12141"
12142        );
12143
12144        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12145        let sketch = expect_sketch(sketch_object);
12146        assert_eq!(sketch.constraints.len(), 1);
12147
12148        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12149        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12150            panic!("Expected constraint object");
12151        };
12152        let Constraint::Parallel(parallel) = constraint else {
12153            panic!("Expected parallel constraint");
12154        };
12155        assert_eq!(parallel.lines.len(), 3);
12156
12157        ctx.close().await;
12158        mock_ctx.close().await;
12159    }
12160
12161    #[tokio::test(flavor = "multi_thread")]
12162    async fn test_lines_perpendicular() {
12163        let initial_source = "\
12164sketch(on = XY) {
12165  line(start = [var 1, var 2], end = [var 3, var 4])
12166  line(start = [var 5, var 6], end = [var 7, var 8])
12167}
12168";
12169
12170        let program = Program::parse(initial_source).unwrap().0.unwrap();
12171
12172        let mut frontend = FrontendState::new();
12173
12174        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12175        let mock_ctx = ExecutorContext::new_mock(None).await;
12176        let version = Version(0);
12177
12178        frontend.hack_set_program(&ctx, program).await.unwrap();
12179        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180        let sketch_id = sketch_object.id;
12181        let sketch = expect_sketch(sketch_object);
12182        let line1_id = *sketch.segments.get(2).unwrap();
12183        let line2_id = *sketch.segments.get(5).unwrap();
12184
12185        let constraint = Constraint::Perpendicular(Perpendicular {
12186            lines: vec![line1_id, line2_id],
12187        });
12188        let (src_delta, scene_delta) = frontend
12189            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12190            .await
12191            .unwrap();
12192        assert_eq!(
12193            src_delta.text.as_str(),
12194            "\
12195sketch(on = XY) {
12196  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12197  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12198  perpendicular([line1, line2])
12199}
12200"
12201        );
12202        assert_eq!(
12203            scene_delta.new_graph.objects.len(),
12204            9,
12205            "{:#?}",
12206            scene_delta.new_graph.objects
12207        );
12208
12209        ctx.close().await;
12210        mock_ctx.close().await;
12211    }
12212
12213    #[tokio::test(flavor = "multi_thread")]
12214    async fn test_lines_angle() {
12215        let initial_source = "\
12216sketch(on = XY) {
12217  line(start = [var 1, var 2], end = [var 3, var 4])
12218  line(start = [var 5, var 6], end = [var 7, var 8])
12219}
12220";
12221
12222        let program = Program::parse(initial_source).unwrap().0.unwrap();
12223
12224        let mut frontend = FrontendState::new();
12225
12226        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12227        let mock_ctx = ExecutorContext::new_mock(None).await;
12228        let version = Version(0);
12229
12230        frontend.hack_set_program(&ctx, program).await.unwrap();
12231        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12232        let sketch_id = sketch_object.id;
12233        let sketch = expect_sketch(sketch_object);
12234        let line1_id = *sketch.segments.get(2).unwrap();
12235        let line2_id = *sketch.segments.get(5).unwrap();
12236
12237        let constraint = Constraint::Angle(Angle {
12238            lines: vec![line1_id, line2_id],
12239            angle: Number {
12240                value: 30.0,
12241                units: NumericSuffix::Deg,
12242            },
12243            source: Default::default(),
12244        });
12245        let (src_delta, scene_delta) = frontend
12246            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12247            .await
12248            .unwrap();
12249        assert_eq!(
12250            src_delta.text.as_str(),
12251            // The lack indentation is a formatter bug.
12252            "\
12253sketch(on = XY) {
12254  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12255  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12256  angle([line1, line2]) == 30deg
12257}
12258"
12259        );
12260        assert_eq!(
12261            scene_delta.new_graph.objects.len(),
12262            9,
12263            "{:#?}",
12264            scene_delta.new_graph.objects
12265        );
12266
12267        ctx.close().await;
12268        mock_ctx.close().await;
12269    }
12270
12271    #[tokio::test(flavor = "multi_thread")]
12272    async fn test_segments_tangent() {
12273        let initial_source = "\
12274sketch(on = XY) {
12275  line(start = [var 1, var 2], end = [var 3, var 4])
12276  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12277}
12278";
12279
12280        let program = Program::parse(initial_source).unwrap().0.unwrap();
12281
12282        let mut frontend = FrontendState::new();
12283
12284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12285        let mock_ctx = ExecutorContext::new_mock(None).await;
12286        let version = Version(0);
12287
12288        frontend.hack_set_program(&ctx, program).await.unwrap();
12289        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12290        let sketch_id = sketch_object.id;
12291        let sketch = expect_sketch(sketch_object);
12292        let line1_id = *sketch.segments.get(2).unwrap();
12293        let arc1_id = *sketch.segments.get(6).unwrap();
12294
12295        let constraint = Constraint::Tangent(Tangent {
12296            input: vec![line1_id, arc1_id],
12297        });
12298        let (src_delta, scene_delta) = frontend
12299            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12300            .await
12301            .unwrap();
12302        assert_eq!(
12303            src_delta.text.as_str(),
12304            "\
12305sketch(on = XY) {
12306  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12307  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12308  tangent([line1, arc1])
12309}
12310"
12311        );
12312        assert_eq!(
12313            scene_delta.new_graph.objects.len(),
12314            10,
12315            "{:#?}",
12316            scene_delta.new_graph.objects
12317        );
12318
12319        ctx.close().await;
12320        mock_ctx.close().await;
12321    }
12322
12323    #[tokio::test(flavor = "multi_thread")]
12324    async fn test_point_midpoint() {
12325        let initial_source = "\
12326sketch(on = XY) {
12327  point(at = [var 1, var 1])
12328  line(start = [var 0, var 0], end = [var 6, var 4])
12329}
12330";
12331
12332        let program = Program::parse(initial_source).unwrap().0.unwrap();
12333
12334        let mut frontend = FrontendState::new();
12335
12336        let ctx = ExecutorContext::new_mock(None).await;
12337        let version = Version(0);
12338
12339        frontend.program = program.clone();
12340        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12341        frontend.update_state_after_exec(outcome, true);
12342        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12343        let sketch_id = sketch_object.id;
12344        let sketch = expect_sketch(sketch_object);
12345        let point_id = *sketch.segments.first().unwrap();
12346        let line_id = *sketch.segments.get(3).unwrap();
12347
12348        let constraint = Constraint::Midpoint(Midpoint {
12349            point: point_id,
12350            segment: line_id,
12351        });
12352        let (src_delta, scene_delta) = frontend
12353            .add_constraint(&ctx, version, sketch_id, constraint)
12354            .await
12355            .unwrap();
12356        assert_eq!(
12357            src_delta.text.as_str(),
12358            "\
12359sketch(on = XY) {
12360  point1 = point(at = [var 1, var 1])
12361  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
12362  midpoint(line1, point = point1)
12363}
12364"
12365        );
12366        assert_eq!(
12367            scene_delta.new_graph.objects.len(),
12368            7,
12369            "{:#?}",
12370            scene_delta.new_graph.objects
12371        );
12372
12373        ctx.close().await;
12374    }
12375
12376    #[tokio::test(flavor = "multi_thread")]
12377    async fn test_segments_symmetric() {
12378        let initial_source = "\
12379sketch(on = XY) {
12380  line(start = [var 0, var 0], end = [var 0, var 4])
12381  line(start = [var 4, var 0], end = [var 4, var 4])
12382  line(start = [var 2, var -1], end = [var 2, var 5])
12383}
12384";
12385
12386        let program = Program::parse(initial_source).unwrap().0.unwrap();
12387
12388        let mut frontend = FrontendState::new();
12389
12390        let ctx = ExecutorContext::new_mock(None).await;
12391        let version = Version(0);
12392
12393        frontend.program = program.clone();
12394        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12395        frontend.update_state_after_exec(outcome, true);
12396        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12397        let sketch_id = sketch_object.id;
12398        let sketch = expect_sketch(sketch_object);
12399        let line1_id = *sketch.segments.get(2).unwrap();
12400        let line2_id = *sketch.segments.get(5).unwrap();
12401        let axis_id = *sketch.segments.get(8).unwrap();
12402
12403        let constraint = Constraint::Symmetric(Symmetric {
12404            input: vec![line1_id, line2_id],
12405            axis: axis_id,
12406        });
12407        let (src_delta, scene_delta) = frontend
12408            .add_constraint(&ctx, version, sketch_id, constraint)
12409            .await
12410            .unwrap();
12411        assert_eq!(
12412            src_delta.text.as_str(),
12413            "\
12414sketch(on = XY) {
12415  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
12416  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
12417  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
12418  symmetric([line1, line2], axis = line3)
12419}
12420"
12421        );
12422        assert_eq!(
12423            scene_delta.new_graph.objects.len(),
12424            12,
12425            "{:#?}",
12426            scene_delta.new_graph.objects
12427        );
12428
12429        ctx.close().await;
12430    }
12431
12432    #[tokio::test(flavor = "multi_thread")]
12433    async fn test_point_arc_midpoint() {
12434        let initial_source = "\
12435sketch(on = XY) {
12436  point(at = [var 6, var 3])
12437  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12438}
12439";
12440
12441        let program = Program::parse(initial_source).unwrap().0.unwrap();
12442
12443        let mut frontend = FrontendState::new();
12444
12445        let ctx = ExecutorContext::new_mock(None).await;
12446        let version = Version(0);
12447
12448        frontend.program = program.clone();
12449        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12450        frontend.update_state_after_exec(outcome, true);
12451        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12452        let sketch_id = sketch_object.id;
12453        let sketch = expect_sketch(sketch_object);
12454        let point_id = *sketch.segments.first().unwrap();
12455        let arc_id = *sketch.segments.get(4).unwrap();
12456
12457        let constraint = Constraint::Midpoint(Midpoint {
12458            point: point_id,
12459            segment: arc_id,
12460        });
12461        let (src_delta, scene_delta) = frontend
12462            .add_constraint(&ctx, version, sketch_id, constraint)
12463            .await
12464            .unwrap();
12465        assert_eq!(
12466            src_delta.text.as_str(),
12467            "\
12468sketch(on = XY) {
12469  point1 = point(at = [var 6, var 3])
12470  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12471  midpoint(arc1, point = point1)
12472}
12473"
12474        );
12475        assert_eq!(
12476            scene_delta.new_graph.objects.len(),
12477            8,
12478            "{:#?}",
12479            scene_delta.new_graph.objects
12480        );
12481
12482        ctx.close().await;
12483    }
12484
12485    #[tokio::test(flavor = "multi_thread")]
12486    async fn test_segments_symmetric_arcs() {
12487        let initial_source = "\
12488sketch(on = XY) {
12489  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12490  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12491  line(start = [var 0, var -10], end = [var 0, var 10])
12492}
12493";
12494
12495        let program = Program::parse(initial_source).unwrap().0.unwrap();
12496
12497        let mut frontend = FrontendState::new();
12498
12499        let ctx = ExecutorContext::new_mock(None).await;
12500        let version = Version(0);
12501
12502        frontend.program = program.clone();
12503        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12504        frontend.update_state_after_exec(outcome, true);
12505        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12506        let sketch_id = sketch_object.id;
12507        let sketch = expect_sketch(sketch_object);
12508        let arc1_id = *sketch.segments.get(3).unwrap();
12509        let arc2_id = *sketch.segments.get(7).unwrap();
12510        let axis_id = *sketch.segments.get(10).unwrap();
12511
12512        let constraint = Constraint::Symmetric(Symmetric {
12513            input: vec![arc1_id, arc2_id],
12514            axis: axis_id,
12515        });
12516        let (src_delta, scene_delta) = frontend
12517            .add_constraint(&ctx, version, sketch_id, constraint)
12518            .await
12519            .unwrap();
12520        assert_eq!(
12521            src_delta.text.as_str(),
12522            "\
12523sketch(on = XY) {
12524  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12525  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12526  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
12527  symmetric([arc1, arc2], axis = line1)
12528}
12529"
12530        );
12531        assert_eq!(
12532            scene_delta.new_graph.objects.len(),
12533            14,
12534            "{:#?}",
12535            scene_delta.new_graph.objects
12536        );
12537
12538        ctx.close().await;
12539    }
12540
12541    #[tokio::test(flavor = "multi_thread")]
12542    async fn test_sketch_on_face_simple() {
12543        let initial_source = "\
12544len = 2mm
12545cube = startSketchOn(XY)
12546  |> startProfile(at = [0, 0])
12547  |> line(end = [len, 0], tag = $side)
12548  |> line(end = [0, len])
12549  |> line(end = [-len, 0])
12550  |> line(end = [0, -len])
12551  |> close()
12552  |> extrude(length = len)
12553
12554face = faceOf(cube, face = side)
12555";
12556
12557        let program = Program::parse(initial_source).unwrap().0.unwrap();
12558
12559        let mut frontend = FrontendState::new();
12560
12561        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12562        let mock_ctx = ExecutorContext::new_mock(None).await;
12563        let version = Version(0);
12564
12565        frontend.hack_set_program(&ctx, program).await.unwrap();
12566        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
12567        let face_id = face_object.id;
12568
12569        let sketch_args = SketchCtor {
12570            on: Plane::Object(face_id),
12571        };
12572        let (_src_delta, scene_delta, sketch_id) = frontend
12573            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12574            .await
12575            .unwrap();
12576        assert_eq!(sketch_id, ObjectId(2));
12577        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12578        let sketch_object = &scene_delta.new_graph.objects[2];
12579        assert_eq!(sketch_object.id, ObjectId(2));
12580        assert_eq!(
12581            sketch_object.kind,
12582            ObjectKind::Sketch(Sketch {
12583                args: SketchCtor {
12584                    on: Plane::Object(face_id),
12585                },
12586                plane: face_id,
12587                segments: vec![],
12588                constraints: vec![],
12589            })
12590        );
12591        assert_eq!(scene_delta.new_graph.objects.len(), 8);
12592
12593        ctx.close().await;
12594        mock_ctx.close().await;
12595    }
12596
12597    #[tokio::test(flavor = "multi_thread")]
12598    async fn test_sketch_on_wall_artifact_from_region_extrude() {
12599        let initial_source = "\
12600s = sketch(on = YZ) {
12601  line1 = line(start = [0, 0], end = [0, 1])
12602  line2 = line(start = [0, 1], end = [1, 1])
12603  line3 = line(start = [1, 1], end = [0, 0])
12604}
12605region001 = region(point = [0.1, 0.1], sketch = s)
12606extrude001 = extrude(region001, length = 5)
12607";
12608
12609        let program = Program::parse(initial_source).unwrap().0.unwrap();
12610
12611        let mut frontend = FrontendState::new();
12612        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12613        let version = Version(0);
12614
12615        frontend.hack_set_program(&ctx, program).await.unwrap();
12616        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12617
12618        let sketch_args = SketchCtor {
12619            on: Plane::Object(wall_object_id),
12620        };
12621        let (src_delta, _scene_delta, _sketch_id) = frontend
12622            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12623            .await
12624            .unwrap();
12625        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12626
12627        ctx.close().await;
12628    }
12629
12630    #[tokio::test(flavor = "multi_thread")]
12631    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
12632        let initial_source = "\
12633sketch001 = sketch(on = YZ) {
12634  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
12635  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
12636  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
12637  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
12638  coincident([line1.end, line2.start])
12639  coincident([line2.end, line3.start])
12640  coincident([line3.end, line4.start])
12641  coincident([line4.end, line1.start])
12642  parallel([line2, line4])
12643  parallel([line3, line1])
12644  perpendicular([line1, line2])
12645  horizontal(line3)
12646  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
12647}
12648region001 = region(point = [3.1, 3.74], sketch = sketch001)
12649extrude001 = extrude(region001, length = 5)
12650";
12651
12652        let program = Program::parse(initial_source).unwrap().0.unwrap();
12653
12654        let mut frontend = FrontendState::new();
12655        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12656        let version = Version(0);
12657
12658        frontend.hack_set_program(&ctx, program).await.unwrap();
12659        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12660
12661        let sketch_args = SketchCtor {
12662            on: Plane::Object(wall_object_id),
12663        };
12664        let (src_delta, _scene_delta, _sketch_id) = frontend
12665            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12666            .await
12667            .unwrap();
12668        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12669
12670        ctx.close().await;
12671    }
12672
12673    #[tokio::test(flavor = "multi_thread")]
12674    async fn test_sketch_on_plane_incremental() {
12675        let initial_source = "\
12676len = 2mm
12677cube = startSketchOn(XY)
12678  |> startProfile(at = [0, 0])
12679  |> line(end = [len, 0], tag = $side)
12680  |> line(end = [0, len])
12681  |> line(end = [-len, 0])
12682  |> line(end = [0, -len])
12683  |> close()
12684  |> extrude(length = len)
12685
12686plane = planeOf(cube, face = side)
12687";
12688
12689        let program = Program::parse(initial_source).unwrap().0.unwrap();
12690
12691        let mut frontend = FrontendState::new();
12692
12693        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12694        let mock_ctx = ExecutorContext::new_mock(None).await;
12695        let version = Version(0);
12696
12697        frontend.hack_set_program(&ctx, program).await.unwrap();
12698        // Find the last plane since the first plane is the XY plane.
12699        let plane_object = frontend
12700            .scene_graph
12701            .objects
12702            .iter()
12703            .rev()
12704            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
12705            .unwrap();
12706        let plane_id = plane_object.id;
12707
12708        let sketch_args = SketchCtor {
12709            on: Plane::Object(plane_id),
12710        };
12711        let (src_delta, scene_delta, sketch_id) = frontend
12712            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12713            .await
12714            .unwrap();
12715        assert_eq!(
12716            src_delta.text.as_str(),
12717            "\
12718len = 2mm
12719cube = startSketchOn(XY)
12720  |> startProfile(at = [0, 0])
12721  |> line(end = [len, 0], tag = $side)
12722  |> line(end = [0, len])
12723  |> line(end = [-len, 0])
12724  |> line(end = [0, -len])
12725  |> close()
12726  |> extrude(length = len)
12727
12728plane = planeOf(cube, face = side)
12729sketch001 = sketch(on = plane) {
12730}
12731"
12732        );
12733        assert_eq!(sketch_id, ObjectId(2));
12734        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12735        let sketch_object = &scene_delta.new_graph.objects[2];
12736        assert_eq!(sketch_object.id, ObjectId(2));
12737        assert_eq!(
12738            sketch_object.kind,
12739            ObjectKind::Sketch(Sketch {
12740                args: SketchCtor {
12741                    on: Plane::Object(plane_id),
12742                },
12743                plane: plane_id,
12744                segments: vec![],
12745                constraints: vec![],
12746            })
12747        );
12748        assert_eq!(scene_delta.new_graph.objects.len(), 9);
12749
12750        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
12751        assert_eq!(plane_object.id, plane_id);
12752        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
12753
12754        ctx.close().await;
12755        mock_ctx.close().await;
12756    }
12757
12758    #[tokio::test(flavor = "multi_thread")]
12759    async fn test_new_sketch_uses_unique_variable_name() {
12760        let initial_source = "\
12761sketch1 = sketch(on = XY) {
12762}
12763";
12764
12765        let program = Program::parse(initial_source).unwrap().0.unwrap();
12766
12767        let mut frontend = FrontendState::new();
12768        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12769        let version = Version(0);
12770
12771        frontend.hack_set_program(&ctx, program).await.unwrap();
12772
12773        let sketch_args = SketchCtor {
12774            on: Plane::Default(PlaneName::Yz),
12775        };
12776        let (src_delta, _, _) = frontend
12777            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12778            .await
12779            .unwrap();
12780
12781        assert_eq!(
12782            src_delta.text.as_str(),
12783            "\
12784sketch1 = sketch(on = XY) {
12785}
12786sketch001 = sketch(on = YZ) {
12787}
12788"
12789        );
12790
12791        ctx.close().await;
12792    }
12793
12794    #[tokio::test(flavor = "multi_thread")]
12795    async fn test_new_sketch_twice_using_same_plane() {
12796        let initial_source = "\
12797sketch1 = sketch(on = XY) {
12798}
12799";
12800
12801        let program = Program::parse(initial_source).unwrap().0.unwrap();
12802
12803        let mut frontend = FrontendState::new();
12804        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12805        let version = Version(0);
12806
12807        frontend.hack_set_program(&ctx, program).await.unwrap();
12808
12809        let sketch_args = SketchCtor {
12810            on: Plane::Default(PlaneName::Xy),
12811        };
12812        let (src_delta, _, _) = frontend
12813            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12814            .await
12815            .unwrap();
12816
12817        assert_eq!(
12818            src_delta.text.as_str(),
12819            "\
12820sketch1 = sketch(on = XY) {
12821}
12822sketch001 = sketch(on = XY) {
12823}
12824"
12825        );
12826
12827        ctx.close().await;
12828    }
12829
12830    #[tokio::test(flavor = "multi_thread")]
12831    async fn test_sketch_mode_reuses_cached_on_expression() {
12832        let initial_source = "\
12833width = 2mm
12834sketch(on = offsetPlane(XY, offset = width)) {
12835  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
12836  distance([line1.start, line1.end]) == width
12837}
12838";
12839        let program = Program::parse(initial_source).unwrap().0.unwrap();
12840
12841        let mut frontend = FrontendState::new();
12842        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12843        let mock_ctx = ExecutorContext::new_mock(None).await;
12844        let version = Version(0);
12845        let project_id = ProjectId(0);
12846        let file_id = FileId(0);
12847
12848        frontend.hack_set_program(&ctx, program).await.unwrap();
12849        let initial_object_count = frontend.scene_graph.objects.len();
12850        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
12851            .expect("Expected sketch object to exist")
12852            .id;
12853
12854        // Entering sketch mode should reuse cached `on` expression state
12855        // (offsetPlane result), not fail or create extra on-surface objects.
12856        let scene_delta = frontend
12857            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12858            .await
12859            .unwrap();
12860        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12861
12862        // A follow-up sketch-mode execution should keep the same stable object
12863        // graph shape as well.
12864        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
12865        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12866
12867        ctx.close().await;
12868        mock_ctx.close().await;
12869    }
12870
12871    #[tokio::test(flavor = "multi_thread")]
12872    async fn test_multiple_sketch_blocks() {
12873        let initial_source = "\
12874// Cube that requires the engine.
12875width = 2
12876sketch001 = startSketchOn(XY)
12877profile001 = startProfile(sketch001, at = [0, 0])
12878  |> yLine(length = width, tag = $seg1)
12879  |> xLine(length = width)
12880  |> yLine(length = -width)
12881  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12882  |> close()
12883extrude001 = extrude(profile001, length = width)
12884
12885// Get a value that requires the engine.
12886x = segLen(seg1)
12887
12888// Triangle with side length 2*x.
12889sketch(on = XY) {
12890  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12891  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12892  coincident([line1.end, line2.start])
12893  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12894  coincident([line2.end, line3.start])
12895  coincident([line3.end, line1.start])
12896  equalLength([line3, line1])
12897  equalLength([line1, line2])
12898  distance([line1.start, line1.end]) == 2*x
12899}
12900
12901// Line segment with length x.
12902sketch2 = sketch(on = XY) {
12903  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12904  distance([line1.start, line1.end]) == x
12905}
12906";
12907
12908        let program = Program::parse(initial_source).unwrap().0.unwrap();
12909
12910        let mut frontend = FrontendState::new();
12911
12912        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12913        let mock_ctx = ExecutorContext::new_mock(None).await;
12914        let version = Version(0);
12915        let project_id = ProjectId(0);
12916        let file_id = FileId(0);
12917
12918        frontend.hack_set_program(&ctx, program).await.unwrap();
12919        let sketch_objects = frontend
12920            .scene_graph
12921            .objects
12922            .iter()
12923            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12924            .collect::<Vec<_>>();
12925        let sketch1_id = sketch_objects.first().unwrap().id;
12926        let sketch2_id = sketch_objects.get(1).unwrap().id;
12927        // First point in sketch1.
12928        let point1_id = ObjectId(sketch1_id.0 + 1);
12929        // First point in sketch2.
12930        let point2_id = ObjectId(sketch2_id.0 + 1);
12931
12932        // Edit the first sketch. Objects before the sketch block should be
12933        // present from execution cache so that we can sketch on prior planes,
12934        // for example. Objects after the first sketch block should not be
12935        // present since those statements are skipped in sketch mode.
12936        //
12937        // - startSketchOn(XY) Plane 1
12938        // - sketch on=XY Plane 1
12939        // - Sketch block 16
12940        let scene_delta = frontend
12941            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12942            .await
12943            .unwrap();
12944        assert_eq!(
12945            scene_delta.new_graph.objects.len(),
12946            18,
12947            "{:#?}",
12948            scene_delta.new_graph.objects
12949        );
12950
12951        // Edit a point in the first sketch.
12952        let point_ctor = PointCtor {
12953            position: Point2d {
12954                x: Expr::Var(Number {
12955                    value: 1.0,
12956                    units: NumericSuffix::Mm,
12957                }),
12958                y: Expr::Var(Number {
12959                    value: 2.0,
12960                    units: NumericSuffix::Mm,
12961                }),
12962            },
12963        };
12964        let segments = vec![ExistingSegmentCtor {
12965            id: point1_id,
12966            ctor: SegmentCtor::Point(point_ctor),
12967        }];
12968        let (src_delta, _) = frontend
12969            .edit_segments(&mock_ctx, version, sketch1_id, segments)
12970            .await
12971            .unwrap();
12972        // Only the first sketch block changes.
12973        assert_eq!(
12974            src_delta.text.as_str(),
12975            "\
12976// Cube that requires the engine.
12977width = 2
12978sketch001 = startSketchOn(XY)
12979profile001 = startProfile(sketch001, at = [0, 0])
12980  |> yLine(length = width, tag = $seg1)
12981  |> xLine(length = width)
12982  |> yLine(length = -width)
12983  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12984  |> close()
12985extrude001 = extrude(profile001, length = width)
12986
12987// Get a value that requires the engine.
12988x = segLen(seg1)
12989
12990// Triangle with side length 2*x.
12991sketch(on = XY) {
12992  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12993  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12994  coincident([line1.end, line2.start])
12995  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12996  coincident([line2.end, line3.start])
12997  coincident([line3.end, line1.start])
12998  equalLength([line3, line1])
12999  equalLength([line1, line2])
13000  distance([line1.start, line1.end]) == 2 * x
13001}
13002
13003// Line segment with length x.
13004sketch2 = sketch(on = XY) {
13005  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13006  distance([line1.start, line1.end]) == x
13007}
13008"
13009        );
13010
13011        // Execute mock to simulate drag end.
13012        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
13013        // Only the first sketch block changes.
13014        assert_eq!(
13015            src_delta.text.as_str(),
13016            "\
13017// Cube that requires the engine.
13018width = 2
13019sketch001 = startSketchOn(XY)
13020profile001 = startProfile(sketch001, at = [0, 0])
13021  |> yLine(length = width, tag = $seg1)
13022  |> xLine(length = width)
13023  |> yLine(length = -width)
13024  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13025  |> close()
13026extrude001 = extrude(profile001, length = width)
13027
13028// Get a value that requires the engine.
13029x = segLen(seg1)
13030
13031// Triangle with side length 2*x.
13032sketch(on = XY) {
13033  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13034  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13035  coincident([line1.end, line2.start])
13036  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13037  coincident([line2.end, line3.start])
13038  coincident([line3.end, line1.start])
13039  equalLength([line3, line1])
13040  equalLength([line1, line2])
13041  distance([line1.start, line1.end]) == 2 * x
13042}
13043
13044// Line segment with length x.
13045sketch2 = sketch(on = XY) {
13046  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13047  distance([line1.start, line1.end]) == x
13048}
13049"
13050        );
13051        // Exit sketch. Objects from the entire program should be present.
13052        //
13053        // - startSketchOn(XY) Plane 1
13054        // - sketch on=XY Plane 1
13055        // - Sketch block 16
13056        // - sketch on=XY cached
13057        // - Sketch block 5
13058        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13059        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
13060
13061        // Edit the second sketch.
13062        //
13063        // - startSketchOn(XY) Plane 1
13064        // - sketch on=XY Plane 1
13065        // - Sketch block 16
13066        // - sketch on=XY cached
13067        // - Sketch block 5
13068        let scene_delta = frontend
13069            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13070            .await
13071            .unwrap();
13072        assert_eq!(
13073            scene_delta.new_graph.objects.len(),
13074            24,
13075            "{:#?}",
13076            scene_delta.new_graph.objects
13077        );
13078
13079        // Edit a point in the second sketch.
13080        let point_ctor = PointCtor {
13081            position: Point2d {
13082                x: Expr::Var(Number {
13083                    value: 3.0,
13084                    units: NumericSuffix::Mm,
13085                }),
13086                y: Expr::Var(Number {
13087                    value: 4.0,
13088                    units: NumericSuffix::Mm,
13089                }),
13090            },
13091        };
13092        let segments = vec![ExistingSegmentCtor {
13093            id: point2_id,
13094            ctor: SegmentCtor::Point(point_ctor),
13095        }];
13096        let (src_delta, _) = frontend
13097            .edit_segments(&mock_ctx, version, sketch2_id, segments)
13098            .await
13099            .unwrap();
13100        // Only the second sketch block changes.
13101        assert_eq!(
13102            src_delta.text.as_str(),
13103            "\
13104// Cube that requires the engine.
13105width = 2
13106sketch001 = startSketchOn(XY)
13107profile001 = startProfile(sketch001, at = [0, 0])
13108  |> yLine(length = width, tag = $seg1)
13109  |> xLine(length = width)
13110  |> yLine(length = -width)
13111  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13112  |> close()
13113extrude001 = extrude(profile001, length = width)
13114
13115// Get a value that requires the engine.
13116x = segLen(seg1)
13117
13118// Triangle with side length 2*x.
13119sketch(on = XY) {
13120  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13121  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13122  coincident([line1.end, line2.start])
13123  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13124  coincident([line2.end, line3.start])
13125  coincident([line3.end, line1.start])
13126  equalLength([line3, line1])
13127  equalLength([line1, line2])
13128  distance([line1.start, line1.end]) == 2 * x
13129}
13130
13131// Line segment with length x.
13132sketch2 = sketch(on = XY) {
13133  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
13134  distance([line1.start, line1.end]) == x
13135}
13136"
13137        );
13138
13139        // Execute mock to simulate drag end.
13140        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
13141        // Only the second sketch block changes.
13142        assert_eq!(
13143            src_delta.text.as_str(),
13144            "\
13145// Cube that requires the engine.
13146width = 2
13147sketch001 = startSketchOn(XY)
13148profile001 = startProfile(sketch001, at = [0, 0])
13149  |> yLine(length = width, tag = $seg1)
13150  |> xLine(length = width)
13151  |> yLine(length = -width)
13152  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13153  |> close()
13154extrude001 = extrude(profile001, length = width)
13155
13156// Get a value that requires the engine.
13157x = segLen(seg1)
13158
13159// Triangle with side length 2*x.
13160sketch(on = XY) {
13161  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13162  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13163  coincident([line1.end, line2.start])
13164  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13165  coincident([line2.end, line3.start])
13166  coincident([line3.end, line1.start])
13167  equalLength([line3, line1])
13168  equalLength([line1, line2])
13169  distance([line1.start, line1.end]) == 2 * x
13170}
13171
13172// Line segment with length x.
13173sketch2 = sketch(on = XY) {
13174  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
13175  distance([line1.start, line1.end]) == x
13176}
13177"
13178        );
13179
13180        ctx.close().await;
13181        mock_ctx.close().await;
13182    }
13183
13184    #[tokio::test(flavor = "multi_thread")]
13185    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
13186        clear_mem_cache().await;
13187
13188        let source = r#"sketch001 = sketch(on = XZ) {
13189  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
13190}
13191sketch002 = sketch(on = XY) {
13192  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
13193  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
13194  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
13195  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
13196  coincident([line1.end, line2.start])
13197  coincident([line2.end, line3.start])
13198  coincident([line3.end, line4.start])
13199  coincident([line4.end, line1.start])
13200  parallel([line2, line4])
13201  parallel([line3, line1])
13202  perpendicular([line1, line2])
13203  horizontal(line3)
13204  coincident([line1.start, ORIGIN])
13205}
13206"#;
13207
13208        let program = Program::parse(source).unwrap().0.unwrap();
13209        let mut frontend = FrontendState::new();
13210        let ctx = ExecutorContext::new_with_engine(
13211            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
13212            Default::default(),
13213        );
13214        let mock_ctx = ExecutorContext::new_mock(None).await;
13215        let version = Version(0);
13216        let project_id = ProjectId(0);
13217        let file_id = FileId(0);
13218
13219        frontend.hack_set_program(&ctx, program).await.unwrap();
13220        let sketch_objects = frontend
13221            .scene_graph
13222            .objects
13223            .iter()
13224            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
13225            .collect::<Vec<_>>();
13226        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
13227
13228        let sketch1_id = sketch_objects[0].id;
13229        let sketch2_id = sketch_objects[1].id;
13230
13231        frontend
13232            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13233            .await
13234            .unwrap();
13235        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13236
13237        let scene_delta = frontend
13238            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13239            .await
13240            .unwrap();
13241        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
13242
13243        clear_mem_cache().await;
13244        ctx.close().await;
13245        mock_ctx.close().await;
13246    }
13247
13248    // Regression tests: operations on source code with extra whitespace/newlines.
13249    // These test that NodePath-based lookups work correctly when source ranges
13250    // are shifted by extra whitespace that wouldn't be present after formatting.
13251
13252    #[tokio::test(flavor = "multi_thread")]
13253    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
13254        // Extra newlines after @settings line - this shifts all source ranges.
13255        let initial_source = "@settings(defaultLengthUnit = mm)
13256
13257
13258
13259sketch001 = sketch(on = XY) {
13260  point(at = [1in, 2in])
13261}
13262";
13263
13264        let program = Program::parse(initial_source).unwrap().0.unwrap();
13265        let mut frontend = FrontendState::new();
13266
13267        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13268        let mock_ctx = ExecutorContext::new_mock(None).await;
13269        let version = Version(0);
13270        let project_id = ProjectId(0);
13271        let file_id = FileId(0);
13272
13273        frontend.hack_set_program(&ctx, program).await.unwrap();
13274        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13275        let sketch_id = sketch_object.id;
13276
13277        // Edit sketch should succeed despite extra newlines.
13278        frontend
13279            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13280            .await
13281            .unwrap();
13282
13283        // Add a new point to the sketch.
13284        let point_ctor = PointCtor {
13285            position: Point2d {
13286                x: Expr::Number(Number {
13287                    value: 5.0,
13288                    units: NumericSuffix::Mm,
13289                }),
13290                y: Expr::Number(Number {
13291                    value: 6.0,
13292                    units: NumericSuffix::Mm,
13293                }),
13294            },
13295        };
13296        let segment = SegmentCtor::Point(point_ctor);
13297        let (src_delta, scene_delta) = frontend
13298            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13299            .await
13300            .unwrap();
13301        // After adding a point, the source should be reformatted with standard whitespace.
13302        assert!(
13303            src_delta.text.contains("point(at = [5mm, 6mm])"),
13304            "Expected new point in source, got: {}",
13305            src_delta.text
13306        );
13307        assert!(!scene_delta.new_objects.is_empty());
13308
13309        ctx.close().await;
13310        mock_ctx.close().await;
13311    }
13312
13313    #[tokio::test(flavor = "multi_thread")]
13314    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
13315        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
13316
13317        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
13318        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
13319
13320        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
13321        assert!(
13322            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
13323            "Expected experimental settings to be added to source"
13324        );
13325    }
13326
13327    #[tokio::test(flavor = "multi_thread")]
13328    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
13329        // Extra newlines after @settings, with an empty sketch block.
13330        let initial_source = "@settings(defaultLengthUnit = mm)
13331
13332
13333
13334s = sketch(on = XY) {}
13335";
13336
13337        let program = Program::parse(initial_source).unwrap().0.unwrap();
13338        let mut frontend = FrontendState::new();
13339
13340        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13341        let mock_ctx = ExecutorContext::new_mock(None).await;
13342        let version = Version(0);
13343
13344        frontend.hack_set_program(&ctx, program).await.unwrap();
13345        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13346        let sketch_id = sketch_object.id;
13347
13348        let line_ctor = LineCtor {
13349            start: Point2d {
13350                x: Expr::Number(Number {
13351                    value: 0.0,
13352                    units: NumericSuffix::Mm,
13353                }),
13354                y: Expr::Number(Number {
13355                    value: 0.0,
13356                    units: NumericSuffix::Mm,
13357                }),
13358            },
13359            end: Point2d {
13360                x: Expr::Number(Number {
13361                    value: 10.0,
13362                    units: NumericSuffix::Mm,
13363                }),
13364                y: Expr::Number(Number {
13365                    value: 10.0,
13366                    units: NumericSuffix::Mm,
13367                }),
13368            },
13369            construction: None,
13370        };
13371        let segment = SegmentCtor::Line(line_ctor);
13372        let (src_delta, scene_delta) = frontend
13373            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13374            .await
13375            .unwrap();
13376        assert!(
13377            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13378            "Expected line in source, got: {}",
13379            src_delta.text
13380        );
13381        // Line creates start point, end point, and line segment.
13382        assert_eq!(scene_delta.new_objects.len(), 3);
13383
13384        ctx.close().await;
13385        mock_ctx.close().await;
13386    }
13387
13388    #[tokio::test(flavor = "multi_thread")]
13389    async fn test_extra_newlines_between_operations_edit_line() {
13390        // Extra newlines between @settings and sketch, and inside the sketch block.
13391        let initial_source = "@settings(defaultLengthUnit = mm)
13392
13393
13394sketch001 = sketch(on = XY) {
13395
13396  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13397
13398}
13399";
13400
13401        let program = Program::parse(initial_source).unwrap().0.unwrap();
13402        let mut frontend = FrontendState::new();
13403
13404        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13405        let mock_ctx = ExecutorContext::new_mock(None).await;
13406        let version = Version(0);
13407        let project_id = ProjectId(0);
13408        let file_id = FileId(0);
13409
13410        frontend.hack_set_program(&ctx, program).await.unwrap();
13411        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13412        let sketch_id = sketch_object.id;
13413        let sketch = expect_sketch(sketch_object);
13414
13415        // Extract segment IDs before edit_sketch borrows frontend mutably.
13416        let line_id = sketch
13417            .segments
13418            .iter()
13419            .copied()
13420            .find(|seg_id| {
13421                matches!(
13422                    &frontend.scene_graph.objects[seg_id.0].kind,
13423                    ObjectKind::Segment {
13424                        segment: Segment::Line(_)
13425                    }
13426                )
13427            })
13428            .expect("Expected a line segment in sketch");
13429
13430        // Enter sketch edit mode.
13431        frontend
13432            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13433            .await
13434            .unwrap();
13435
13436        // Edit the line.
13437        let line_ctor = LineCtor {
13438            start: Point2d {
13439                x: Expr::Var(Number {
13440                    value: 1.0,
13441                    units: NumericSuffix::Mm,
13442                }),
13443                y: Expr::Var(Number {
13444                    value: 2.0,
13445                    units: NumericSuffix::Mm,
13446                }),
13447            },
13448            end: Point2d {
13449                x: Expr::Var(Number {
13450                    value: 13.0,
13451                    units: NumericSuffix::Mm,
13452                }),
13453                y: Expr::Var(Number {
13454                    value: 14.0,
13455                    units: NumericSuffix::Mm,
13456                }),
13457            },
13458            construction: None,
13459        };
13460        let segments = vec![ExistingSegmentCtor {
13461            id: line_id,
13462            ctor: SegmentCtor::Line(line_ctor),
13463        }];
13464        let (src_delta, _scene_delta) = frontend
13465            .edit_segments(&mock_ctx, version, sketch_id, segments)
13466            .await
13467            .unwrap();
13468        assert!(
13469            src_delta
13470                .text
13471                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
13472            "Expected edited line in source, got: {}",
13473            src_delta.text
13474        );
13475
13476        ctx.close().await;
13477        mock_ctx.close().await;
13478    }
13479
13480    #[tokio::test(flavor = "multi_thread")]
13481    async fn test_extra_newlines_delete_segment() {
13482        // Extra whitespace before and after the sketch block.
13483        let initial_source = "@settings(defaultLengthUnit = mm)
13484
13485
13486
13487sketch001 = sketch(on = XY) {
13488  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13489}
13490";
13491
13492        let program = Program::parse(initial_source).unwrap().0.unwrap();
13493        let mut frontend = FrontendState::new();
13494
13495        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13496        let mock_ctx = ExecutorContext::new_mock(None).await;
13497        let version = Version(0);
13498
13499        frontend.hack_set_program(&ctx, program).await.unwrap();
13500        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13501        let sketch_id = sketch_object.id;
13502        let sketch = expect_sketch(sketch_object);
13503
13504        // The sketch should have 3 segments: start point, center point, and the circle.
13505        assert_eq!(sketch.segments.len(), 3);
13506        let circle_id = sketch.segments[2];
13507
13508        // Delete the circle despite extra newlines in original source.
13509        let (src_delta, scene_delta) = frontend
13510            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
13511            .await
13512            .unwrap();
13513        assert!(
13514            src_delta.text.contains("sketch(on = XY) {"),
13515            "Expected sketch block in source, got: {}",
13516            src_delta.text
13517        );
13518        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13519        let new_sketch = expect_sketch(new_sketch_object);
13520        assert_eq!(new_sketch.segments.len(), 0);
13521
13522        ctx.close().await;
13523        mock_ctx.close().await;
13524    }
13525
13526    #[tokio::test(flavor = "multi_thread")]
13527    async fn test_unformatted_source_add_arc() {
13528        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
13529        let initial_source = "@settings(defaultLengthUnit = mm)
13530
13531
13532
13533
13534sketch001 = sketch(on = XY) {
13535}
13536";
13537
13538        let program = Program::parse(initial_source).unwrap().0.unwrap();
13539        let mut frontend = FrontendState::new();
13540
13541        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13542        let mock_ctx = ExecutorContext::new_mock(None).await;
13543        let version = Version(0);
13544
13545        frontend.hack_set_program(&ctx, program).await.unwrap();
13546        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13547        let sketch_id = sketch_object.id;
13548
13549        let arc_ctor = ArcCtor {
13550            start: Point2d {
13551                x: Expr::Var(Number {
13552                    value: 5.0,
13553                    units: NumericSuffix::Mm,
13554                }),
13555                y: Expr::Var(Number {
13556                    value: 0.0,
13557                    units: NumericSuffix::Mm,
13558                }),
13559            },
13560            end: Point2d {
13561                x: Expr::Var(Number {
13562                    value: 0.0,
13563                    units: NumericSuffix::Mm,
13564                }),
13565                y: Expr::Var(Number {
13566                    value: 5.0,
13567                    units: NumericSuffix::Mm,
13568                }),
13569            },
13570            center: Point2d {
13571                x: Expr::Var(Number {
13572                    value: 0.0,
13573                    units: NumericSuffix::Mm,
13574                }),
13575                y: Expr::Var(Number {
13576                    value: 0.0,
13577                    units: NumericSuffix::Mm,
13578                }),
13579            },
13580            construction: None,
13581        };
13582        let segment = SegmentCtor::Arc(arc_ctor);
13583        let (src_delta, scene_delta) = frontend
13584            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13585            .await
13586            .unwrap();
13587        assert!(
13588            src_delta
13589                .text
13590                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
13591            "Expected arc in source, got: {}",
13592            src_delta.text
13593        );
13594        assert!(!scene_delta.new_objects.is_empty());
13595
13596        ctx.close().await;
13597        mock_ctx.close().await;
13598    }
13599
13600    #[tokio::test(flavor = "multi_thread")]
13601    async fn test_extra_newlines_add_circle() {
13602        // Extra blank lines between settings and sketch.
13603        let initial_source = "@settings(defaultLengthUnit = mm)
13604
13605
13606
13607sketch001 = sketch(on = XY) {
13608}
13609";
13610
13611        let program = Program::parse(initial_source).unwrap().0.unwrap();
13612        let mut frontend = FrontendState::new();
13613
13614        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13615        let mock_ctx = ExecutorContext::new_mock(None).await;
13616        let version = Version(0);
13617
13618        frontend.hack_set_program(&ctx, program).await.unwrap();
13619        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13620        let sketch_id = sketch_object.id;
13621
13622        let circle_ctor = CircleCtor {
13623            start: Point2d {
13624                x: Expr::Var(Number {
13625                    value: 5.0,
13626                    units: NumericSuffix::Mm,
13627                }),
13628                y: Expr::Var(Number {
13629                    value: 0.0,
13630                    units: NumericSuffix::Mm,
13631                }),
13632            },
13633            center: Point2d {
13634                x: Expr::Var(Number {
13635                    value: 0.0,
13636                    units: NumericSuffix::Mm,
13637                }),
13638                y: Expr::Var(Number {
13639                    value: 0.0,
13640                    units: NumericSuffix::Mm,
13641                }),
13642            },
13643            construction: None,
13644        };
13645        let segment = SegmentCtor::Circle(circle_ctor);
13646        let (src_delta, scene_delta) = frontend
13647            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13648            .await
13649            .unwrap();
13650        assert!(
13651            src_delta
13652                .text
13653                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
13654            "Expected circle in source, got: {}",
13655            src_delta.text
13656        );
13657        assert!(!scene_delta.new_objects.is_empty());
13658
13659        ctx.close().await;
13660        mock_ctx.close().await;
13661    }
13662
13663    #[tokio::test(flavor = "multi_thread")]
13664    async fn test_extra_newlines_add_constraint() {
13665        // Extra newlines with a sketch containing two lines - add a coincident constraint.
13666        let initial_source = "@settings(defaultLengthUnit = mm)
13667
13668
13669
13670sketch001 = sketch(on = XY) {
13671  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13672  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
13673}
13674";
13675
13676        let program = Program::parse(initial_source).unwrap().0.unwrap();
13677        let mut frontend = FrontendState::new();
13678
13679        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13680        let mock_ctx = ExecutorContext::new_mock(None).await;
13681        let version = Version(0);
13682        let project_id = ProjectId(0);
13683        let file_id = FileId(0);
13684
13685        frontend.hack_set_program(&ctx, program).await.unwrap();
13686        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13687        let sketch_id = sketch_object.id;
13688        let sketch = expect_sketch(sketch_object);
13689
13690        // Extract segment data before edit_sketch borrows frontend mutably.
13691        let line_ids: Vec<ObjectId> = sketch
13692            .segments
13693            .iter()
13694            .copied()
13695            .filter(|seg_id| {
13696                matches!(
13697                    &frontend.scene_graph.objects[seg_id.0].kind,
13698                    ObjectKind::Segment {
13699                        segment: Segment::Line(_)
13700                    }
13701                )
13702            })
13703            .collect();
13704        assert_eq!(line_ids.len(), 2, "Expected two line segments");
13705
13706        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
13707        let ObjectKind::Segment {
13708            segment: Segment::Line(line1_data),
13709        } = &line1.kind
13710        else {
13711            panic!("Expected line");
13712        };
13713        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
13714        let ObjectKind::Segment {
13715            segment: Segment::Line(line2_data),
13716        } = &line2.kind
13717        else {
13718            panic!("Expected line");
13719        };
13720
13721        // Build constraint before entering sketch mode.
13722        let constraint = Constraint::Coincident(Coincident {
13723            segments: vec![line1_data.end.into(), line2_data.start.into()],
13724        });
13725
13726        // Enter sketch edit mode.
13727        frontend
13728            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13729            .await
13730            .unwrap();
13731        let (src_delta, _scene_delta) = frontend
13732            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13733            .await
13734            .unwrap();
13735        assert!(
13736            src_delta.text.contains("coincident("),
13737            "Expected coincident constraint in source, got: {}",
13738            src_delta.text
13739        );
13740
13741        ctx.close().await;
13742        mock_ctx.close().await;
13743    }
13744
13745    #[tokio::test(flavor = "multi_thread")]
13746    async fn test_extra_newlines_add_line_then_edit_line() {
13747        // Extra newlines after @settings - add a line, then edit it.
13748        let initial_source = "@settings(defaultLengthUnit = mm)
13749
13750
13751
13752sketch001 = sketch(on = XY) {
13753}
13754";
13755
13756        let program = Program::parse(initial_source).unwrap().0.unwrap();
13757        let mut frontend = FrontendState::new();
13758
13759        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13760        let mock_ctx = ExecutorContext::new_mock(None).await;
13761        let version = Version(0);
13762
13763        frontend.hack_set_program(&ctx, program).await.unwrap();
13764        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13765        let sketch_id = sketch_object.id;
13766
13767        // Add a line.
13768        let line_ctor = LineCtor {
13769            start: Point2d {
13770                x: Expr::Number(Number {
13771                    value: 0.0,
13772                    units: NumericSuffix::Mm,
13773                }),
13774                y: Expr::Number(Number {
13775                    value: 0.0,
13776                    units: NumericSuffix::Mm,
13777                }),
13778            },
13779            end: Point2d {
13780                x: Expr::Number(Number {
13781                    value: 10.0,
13782                    units: NumericSuffix::Mm,
13783                }),
13784                y: Expr::Number(Number {
13785                    value: 10.0,
13786                    units: NumericSuffix::Mm,
13787                }),
13788            },
13789            construction: None,
13790        };
13791        let segment = SegmentCtor::Line(line_ctor);
13792        let (src_delta, scene_delta) = frontend
13793            .add_segment(&mock_ctx, version, sketch_id, segment, None)
13794            .await
13795            .unwrap();
13796        assert!(
13797            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13798            "Expected line in source after add, got: {}",
13799            src_delta.text
13800        );
13801        // Line creates start point, end point, and line segment.
13802        let line_id = *scene_delta.new_objects.last().unwrap();
13803
13804        // Edit the line.
13805        let line_ctor = LineCtor {
13806            start: Point2d {
13807                x: Expr::Number(Number {
13808                    value: 1.0,
13809                    units: NumericSuffix::Mm,
13810                }),
13811                y: Expr::Number(Number {
13812                    value: 2.0,
13813                    units: NumericSuffix::Mm,
13814                }),
13815            },
13816            end: Point2d {
13817                x: Expr::Number(Number {
13818                    value: 13.0,
13819                    units: NumericSuffix::Mm,
13820                }),
13821                y: Expr::Number(Number {
13822                    value: 14.0,
13823                    units: NumericSuffix::Mm,
13824                }),
13825            },
13826            construction: None,
13827        };
13828        let segments = vec![ExistingSegmentCtor {
13829            id: line_id,
13830            ctor: SegmentCtor::Line(line_ctor),
13831        }];
13832        let (src_delta, scene_delta) = frontend
13833            .edit_segments(&mock_ctx, version, sketch_id, segments)
13834            .await
13835            .unwrap();
13836        assert!(
13837            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
13838            "Expected edited line in source, got: {}",
13839            src_delta.text
13840        );
13841        assert_eq!(scene_delta.new_objects, vec![]);
13842
13843        ctx.close().await;
13844        mock_ctx.close().await;
13845    }
13846}