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_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::SegmentDragAnchor;
19use crate::collections::AhashIndexSet;
20use crate::execution::Artifact;
21use crate::execution::ArtifactGraph;
22use crate::execution::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::CapSource;
53use crate::frontend::api::Expr;
54use crate::frontend::api::FileId;
55use crate::frontend::api::Number;
56use crate::frontend::api::ObjectId;
57use crate::frontend::api::ObjectKind;
58use crate::frontend::api::Plane;
59use crate::frontend::api::ProjectId;
60use crate::frontend::api::RestoreSketchCheckpointOutcome;
61use crate::frontend::api::SceneGraph;
62use crate::frontend::api::SceneGraphDelta;
63use crate::frontend::api::SketchCheckpointId;
64use crate::frontend::api::SourceDelta;
65use crate::frontend::api::SourceRef;
66use crate::frontend::api::SourceRefRange;
67use crate::frontend::api::Version;
68use crate::frontend::api::WallSource;
69use crate::frontend::modify::find_defined_names;
70use crate::frontend::modify::next_free_name;
71use crate::frontend::modify::next_free_name_with_padding;
72use crate::frontend::sketch::Coincident;
73use crate::frontend::sketch::Constraint;
74use crate::frontend::sketch::ConstraintSegment;
75use crate::frontend::sketch::Diameter;
76use crate::frontend::sketch::ExistingSegmentCtor;
77use crate::frontend::sketch::Horizontal;
78use crate::frontend::sketch::LineCtor;
79use crate::frontend::sketch::Point2d;
80use crate::frontend::sketch::Radius;
81use crate::frontend::sketch::Segment;
82use crate::frontend::sketch::SegmentCtor;
83use crate::frontend::sketch::SketchApi;
84use crate::frontend::sketch::SketchCtor;
85use crate::frontend::sketch::Vertical;
86use crate::frontend::traverse::MutateBodyItem;
87use crate::frontend::traverse::TraversalReturn;
88use crate::frontend::traverse::Visitor;
89use crate::frontend::traverse::dfs_mut;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::NodePathExt;
93use crate::pretty::NumericSuffix;
94use crate::std::constraints::LinesAtAngleKind;
95use crate::walk::NodeMut;
96use crate::walk::Visitable;
97
98pub(crate) mod api;
99pub(crate) mod modify;
100pub(crate) mod sketch;
101
102pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
103
104#[derive(Debug, Clone)]
105struct SketchCheckpoint {
106    id: SketchCheckpointId,
107    source: SourceDelta,
108    program: Program,
109    scene_graph: SceneGraph,
110    exec_outcome: ExecOutcome,
111    point_freedom_cache: HashMap<ObjectId, Freedom>,
112    mock_memory: Option<SketchModeState>,
113}
114mod traverse;
115pub(crate) mod trim;
116
117struct ArcSizeConstraintParams {
118    points: Vec<ObjectId>,
119    function_name: &'static str,
120    value: f64,
121    units: NumericSuffix,
122    label_position: Option<Point2d<Number>>,
123    constraint_type_name: &'static str,
124}
125
126const POINT_FN: &str = "point";
127const POINT_AT_PARAM: &str = "at";
128const LINE_FN: &str = "line";
129const LINE_VARIABLE: &str = "line";
130const LINE_START_PARAM: &str = "start";
131const LINE_END_PARAM: &str = "end";
132const ARC_FN: &str = "arc";
133const ARC_VARIABLE: &str = "arc";
134const ARC_START_PARAM: &str = "start";
135const ARC_END_PARAM: &str = "end";
136const ARC_CENTER_PARAM: &str = "center";
137const CIRCLE_FN: &str = "circle";
138const CIRCLE_VARIABLE: &str = "circle";
139const CIRCLE_START_PARAM: &str = "start";
140const CIRCLE_CENTER_PARAM: &str = "center";
141const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
142const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
143const LABEL_POSITION_PARAM: &str = "labelPosition";
144
145const COINCIDENT_FN: &str = "coincident";
146const DIAMETER_FN: &str = "diameter";
147const DISTANCE_FN: &str = "distance";
148const FIXED_FN: &str = "fixed";
149const ANGLE_FN: &str = "angle";
150const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
151const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
152const EQUAL_LENGTH_FN: &str = "equalLength";
153const EQUAL_RADIUS_FN: &str = "equalRadius";
154const HORIZONTAL_FN: &str = "horizontal";
155const MIDPOINT_FN: &str = "midpoint";
156const MIDPOINT_POINT_PARAM: &str = "point";
157const RADIUS_FN: &str = "radius";
158const SYMMETRIC_FN: &str = "symmetric";
159const SYMMETRIC_AXIS_PARAM: &str = "axis";
160const TANGENT_FN: &str = "tangent";
161const VERTICAL_FN: &str = "vertical";
162
163const LINE_PROPERTY_START: &str = "start";
164const LINE_PROPERTY_END: &str = "end";
165
166const ARC_PROPERTY_START: &str = "start";
167const ARC_PROPERTY_END: &str = "end";
168const ARC_PROPERTY_CENTER: &str = "center";
169const CIRCLE_PROPERTY_START: &str = "start";
170const CIRCLE_PROPERTY_CENTER: &str = "center";
171const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
172const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
173
174const CONSTRUCTION_PARAM: &str = "construction";
175
176#[derive(Debug, Clone, Copy)]
177enum EditDeleteKind {
178    Edit,
179    DeleteNonSketch,
180}
181
182/// Options that control how an edit is re-executed and written back.
183struct ExecuteAfterEditOptions {
184    segment_ids_edited: AhashIndexSet<ObjectId>,
185    edit_kind: EditDeleteKind,
186    commit_solved_initial_guesses: bool,
187}
188
189impl EditDeleteKind {
190    /// Returns true if this edit is any type of deletion.
191    fn is_delete(&self) -> bool {
192        match self {
193            EditDeleteKind::Edit => false,
194            EditDeleteKind::DeleteNonSketch => true,
195        }
196    }
197
198    fn to_change_kind(self) -> ChangeKind {
199        match self {
200            EditDeleteKind::Edit => ChangeKind::Edit,
201            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
202        }
203    }
204}
205
206#[derive(Debug, Clone, Copy)]
207enum ChangeKind {
208    Add,
209    Edit,
210    Delete,
211    None,
212}
213
214#[derive(Debug, Clone, Serialize, ts_rs::TS)]
215#[ts(export, export_to = "FrontendApi.ts")]
216#[serde(tag = "type")]
217pub enum SetProgramOutcome {
218    #[serde(rename_all = "camelCase")]
219    Success {
220        scene_graph: Box<SceneGraph>,
221        exec_outcome: Box<ExecOutcome>,
222        checkpoint_id: Option<SketchCheckpointId>,
223    },
224    #[serde(rename_all = "camelCase")]
225    ExecFailure { error: Box<KclErrorWithOutputs> },
226}
227
228/// Options for a sketch segment edit that participates in drag solving.
229pub struct EditSegmentsOptions {
230    /// Narrows which edited scene objects receive temporary fixed constraints.
231    ///
232    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
233    /// disables those fixed constraints, which is useful for semantic edits such
234    /// as toggling construction state.
235    pub anchor_segment_ids: Option<Vec<ObjectId>>,
236    /// Hidden fixed cursor points that the referenced segment bodies must pass
237    /// through during solve.
238    pub drag_anchors: Vec<SegmentDragAnchor>,
239    /// Whether solver-updated initial guesses should be written back to KCL.
240    pub commit_solved_initial_guesses: bool,
241}
242
243/// Options for a distance-constraint label edit during sketch dragging.
244pub struct EditDistanceConstraintLabelPositionOptions {
245    /// Edited scene objects to keep anchored while previewing the label edit.
246    pub anchor_segment_ids: Vec<ObjectId>,
247    /// Whether solver-updated initial guesses should be written back to KCL.
248    pub commit_solved_initial_guesses: bool,
249}
250
251#[derive(Debug, Clone)]
252pub struct FrontendState {
253    program: Program,
254    scene_graph: SceneGraph,
255    /// Stores the last known freedom value for each point object.
256    /// This allows us to preserve freedom values when freedom analysis isn't run.
257    point_freedom_cache: HashMap<ObjectId, Freedom>,
258    /// One-shot drag anchors for the next segment edit. These ids define which
259    /// edited points/segments become temporary fixed constraints during solve.
260    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
261    /// One-shot segment-body drag anchors for the next segment edit. These add
262    /// a temporary solver point on the dragged segment that follows the cursor.
263    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
264    /// One-shot override for whether the next edit commits solver-updated
265    /// initial guesses back into KCL. Drag previews keep this off so only the
266    /// explicit drag edit feeds the next solve.
267    next_edit_commits_solver_solutions: Option<bool>,
268    sketch_checkpoints: VecDeque<SketchCheckpoint>,
269    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
270}
271
272impl Default for FrontendState {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl FrontendState {
279    pub fn new() -> Self {
280        Self {
281            program: Program::empty(),
282            scene_graph: SceneGraph {
283                project: ProjectId(0),
284                file: FileId(0),
285                version: Version(0),
286                objects: Default::default(),
287                settings: Default::default(),
288                sketch_mode: Default::default(),
289            },
290            point_freedom_cache: HashMap::new(),
291            next_drag_anchor_segment_ids: None,
292            next_segment_drag_anchors: None,
293            next_edit_commits_solver_solutions: None,
294            sketch_checkpoints: VecDeque::new(),
295            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
296        }
297    }
298
299    /// Get a reference to the scene graph
300    pub fn scene_graph(&self) -> &SceneGraph {
301        &self.scene_graph
302    }
303
304    pub fn default_length_unit(&self) -> UnitLength {
305        self.program
306            .meta_settings()
307            .ok()
308            .flatten()
309            .map(|settings| settings.default_length_units)
310            .unwrap_or(UnitLength::Millimeters)
311    }
312
313    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
314        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
315
316        let checkpoint = SketchCheckpoint {
317            id: checkpoint_id,
318            source: SourceDelta {
319                text: source_from_ast(&self.program.ast),
320            },
321            program: self.program.clone(),
322            scene_graph: self.scene_graph.clone(),
323            exec_outcome,
324            point_freedom_cache: self.point_freedom_cache.clone(),
325            mock_memory: read_old_memory().await,
326        };
327
328        self.sketch_checkpoints.push_back(checkpoint);
329        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
330            self.sketch_checkpoints.pop_front();
331        }
332
333        Ok(checkpoint_id)
334    }
335
336    /// Edit sketch segments with optional drag-solve overrides.
337    ///
338    /// Drag anchors add hidden fixed cursor points and constrain the referenced
339    /// segment bodies to pass through them, which lets body drags use the same
340    /// anchor model without pinning all child points. Preview callers disable
341    /// solver writeback so solved geometry can be returned without feeding every
342    /// solver value back into KCL.
343    pub async fn edit_segments_with_options(
344        &mut self,
345        ctx: &ExecutorContext,
346        version: Version,
347        sketch: ObjectId,
348        segments: Vec<ExistingSegmentCtor>,
349        options: EditSegmentsOptions,
350    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
351        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
352            self.next_drag_anchor_segment_ids
353                .replace(anchor_ids.into_iter().collect())
354        });
355        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
356        let previous_commit_mode = self
357            .next_edit_commits_solver_solutions
358            .replace(options.commit_solved_initial_guesses);
359        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
360        if let Some(previous_anchor_ids) = previous_anchor_ids {
361            self.next_drag_anchor_segment_ids = previous_anchor_ids;
362        }
363        self.next_segment_drag_anchors = previous_drag_anchors;
364        self.next_edit_commits_solver_solutions = previous_commit_mode;
365        result
366    }
367
368    /// Edit a distance-constraint label position with optional solver writeback.
369    ///
370    /// Drag previews set `commit_solved_initial_guesses` to false so label
371    /// placement can be previewed against solved geometry without advancing
372    /// persistent KCL state until drag completion.
373    pub async fn edit_distance_constraint_label_position_with_options(
374        &mut self,
375        ctx: &ExecutorContext,
376        version: Version,
377        sketch: ObjectId,
378        constraint_id: ObjectId,
379        label_position: Point2d<Number>,
380        options: EditDistanceConstraintLabelPositionOptions,
381    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
382        let previous_commit_mode = self
383            .next_edit_commits_solver_solutions
384            .replace(options.commit_solved_initial_guesses);
385        let result = SketchApi::edit_distance_constraint_label_position(
386            self,
387            ctx,
388            version,
389            sketch,
390            constraint_id,
391            label_position,
392            options.anchor_segment_ids,
393        )
394        .await;
395        self.next_edit_commits_solver_solutions = previous_commit_mode;
396        result
397    }
398
399    pub async fn restore_sketch_checkpoint(
400        &mut self,
401        checkpoint_id: SketchCheckpointId,
402    ) -> api::Result<RestoreSketchCheckpointOutcome> {
403        let checkpoint = self
404            .sketch_checkpoints
405            .iter()
406            .find(|checkpoint| checkpoint.id == checkpoint_id)
407            .cloned()
408            .ok_or_else(|| Error {
409                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
410            })?;
411
412        self.program = checkpoint.program;
413        self.scene_graph = checkpoint.scene_graph.clone();
414        self.point_freedom_cache = checkpoint.point_freedom_cache;
415        self.next_drag_anchor_segment_ids = None;
416        self.next_segment_drag_anchors = None;
417        self.next_edit_commits_solver_solutions = None;
418
419        if let Some(mock_memory) = checkpoint.mock_memory {
420            write_old_memory(mock_memory).await;
421        } else {
422            clear_mem_cache().await;
423        }
424
425        Ok(RestoreSketchCheckpointOutcome {
426            source_delta: checkpoint.source,
427            scene_graph_delta: SceneGraphDelta {
428                new_graph: self.scene_graph_for_ui(),
429                new_objects: Vec::new(),
430                invalidates_ids: true,
431                exec_outcome: checkpoint.exec_outcome,
432            },
433        })
434    }
435
436    pub fn clear_sketch_checkpoints(&mut self) {
437        self.sketch_checkpoints.clear();
438    }
439    fn scene_graph_for_ui(&self) -> SceneGraph {
440        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
441            matches!(
442                object.kind,
443                ObjectKind::Segment {
444                    segment: Segment::ControlPointSpline(_)
445                }
446            )
447        });
448
449        if !has_control_point_splines {
450            return self.scene_graph.clone();
451        }
452
453        let hidden_constraint_ids = self
454            .scene_graph
455            .objects
456            .iter()
457            .filter_map(|object| match &object.kind {
458                ObjectKind::Constraint {
459                    constraint: Constraint::Coincident(coincident),
460                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
461                    Some(object.id)
462                }
463                _ => None,
464            })
465            .collect::<HashSet<_>>();
466
467        if hidden_constraint_ids.is_empty() {
468            return self.scene_graph.clone();
469        }
470
471        let mut scene_graph = self.scene_graph.clone();
472        for object in &mut scene_graph.objects {
473            match &mut object.kind {
474                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
475                    object.kind = ObjectKind::Nil;
476                }
477                ObjectKind::Sketch(sketch) => {
478                    sketch
479                        .constraints
480                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
481                }
482                _ => {}
483            }
484        }
485
486        scene_graph
487    }
488}
489
490fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
491    let mut first_owner_id = None;
492    for segment_id in coincident.segment_ids() {
493        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
494            return false;
495        };
496
497        match first_owner_id {
498            Some(first_owner_id) if first_owner_id != owner_id => return false,
499            Some(_) => {}
500            None => first_owner_id = Some(owner_id),
501        }
502    }
503
504    first_owner_id.is_some()
505}
506
507fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
508    let object = scene_graph.objects.get(segment_id.0)?;
509    let ObjectKind::Segment { segment } = &object.kind else {
510        return None;
511    };
512
513    match segment {
514        Segment::ControlPointSpline(_) => Some(segment_id),
515        Segment::Point(point) => point
516            .owner
517            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
518        Segment::Line(line) => line
519            .owner
520            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
521        _ => None,
522    }
523}
524
525fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
526    matches!(
527        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
528        Some(ObjectKind::Segment {
529            segment: Segment::ControlPointSpline(_)
530        })
531    )
532}
533
534fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
535    let experimental_features_allowed = program
536        .meta_settings()
537        .ok()
538        .flatten()
539        .map(|settings| settings.experimental_features == WarningLevel::Allow)
540        .unwrap_or(false);
541    if experimental_features_allowed {
542        return Ok(program.clone());
543    }
544
545    program.change_experimental_features(Some(WarningLevel::Allow))
546}
547
548impl SketchApi for FrontendState {
549    async fn execute_mock(
550        &mut self,
551        ctx: &ExecutorContext,
552        _version: Version,
553        sketch: ObjectId,
554    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
555        let sketch_block_ref =
556            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
557
558        let mut truncated_program = self.program.clone();
559        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
560            .map_err(KclErrorWithOutputs::no_outputs)?;
561
562        // Execute.
563        let outcome = ctx
564            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
565            .await?;
566        let new_source = source_from_ast(&self.program.ast);
567        let src_delta = SourceDelta { text: new_source };
568        // MockConfig::default() has freedom_analysis: true
569        let outcome = self.update_state_after_exec(outcome, true);
570        let scene_graph_delta = SceneGraphDelta {
571            new_graph: self.scene_graph.clone(),
572            new_objects: Default::default(),
573            invalidates_ids: false,
574            exec_outcome: outcome,
575        };
576        Ok((src_delta, scene_graph_delta))
577    }
578
579    async fn new_sketch(
580        &mut self,
581        ctx: &ExecutorContext,
582        _project: ProjectId,
583        _file: FileId,
584        _version: Version,
585        args: SketchCtor,
586    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
587        // TODO: Check version.
588
589        let mut new_ast = self.program.ast.clone();
590        // Create updated KCL source from args.
591        let mut plane_ast =
592            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
593        let mut defined_names = find_defined_names(&new_ast);
594        let is_face_of_expr = matches!(
595            &plane_ast,
596            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
597        );
598        if is_face_of_expr {
599            let face_name = next_free_name_with_padding("face", &defined_names)
600                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
601            let face_decl = ast::VariableDeclaration::new(
602                ast::VariableDeclarator::new(&face_name, plane_ast),
603                ast::ItemVisibility::Default,
604                ast::VariableKind::Const,
605            );
606            new_ast
607                .body
608                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
609                    face_decl,
610                ))));
611            defined_names.insert(face_name.clone());
612            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
613        }
614        let sketch_ast = ast::SketchBlock {
615            arguments: vec![ast::LabeledArg {
616                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
617                arg: plane_ast,
618            }],
619            body: Default::default(),
620            is_being_edited: false,
621            non_code_meta: Default::default(),
622            digest: None,
623        };
624        // Add a sketch block as a variable declaration directly, avoiding
625        // source-range mutation on a no-src node.
626        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
627            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
628        let sketch_decl = ast::VariableDeclaration::new(
629            ast::VariableDeclarator::new(
630                &sketch_name,
631                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
632            ),
633            ast::ItemVisibility::Default,
634            ast::VariableKind::Const,
635        );
636        new_ast
637            .body
638            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
639                sketch_decl,
640            ))));
641        // Convert to string source to create real source ranges.
642        let new_source = source_from_ast(&new_ast);
643        // Parse the new source.
644        let (new_program, errors) = Program::parse(&new_source)
645            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
646        if !errors.is_empty() {
647            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
648                "Error parsing KCL source after adding sketch: {errors:?}"
649            ))));
650        }
651        let Some(new_program) = new_program else {
652            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
653                "No AST produced after adding sketch".to_owned(),
654            )));
655        };
656
657        // Make sure to only set this if there are no errors.
658        self.program = new_program.clone();
659
660        // We need to do an engine execute so that the plane object gets created
661        // and is cached.
662        let outcome = ctx.run_with_caching(new_program.clone()).await?;
663        let freedom_analysis_ran = true;
664
665        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
666
667        let Some(sketch_id) = self
668            .scene_graph
669            .objects
670            .iter()
671            .filter_map(|object| match object.kind {
672                ObjectKind::Sketch(_) => Some(object.id),
673                _ => None,
674            })
675            .max_by_key(|id| id.0)
676        else {
677            return Err(KclErrorWithOutputs::from_error_outcome(
678                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
679                outcome,
680            ));
681        };
682        // Store the object in the scene.
683        self.scene_graph.sketch_mode = Some(sketch_id);
684
685        let src_delta = SourceDelta { text: new_source };
686        let scene_graph_delta = SceneGraphDelta {
687            new_graph: self.scene_graph_for_ui(),
688            invalidates_ids: false,
689            new_objects: vec![sketch_id],
690            exec_outcome: outcome,
691        };
692        Ok((src_delta, scene_graph_delta, sketch_id))
693    }
694
695    async fn edit_sketch(
696        &mut self,
697        ctx: &ExecutorContext,
698        _project: ProjectId,
699        _file: FileId,
700        _version: Version,
701        sketch: ObjectId,
702    ) -> ExecResult<SceneGraphDelta> {
703        // TODO: Check version.
704
705        // Look up existing sketch.
706        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
707            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
708        })?;
709        let ObjectKind::Sketch(_) = &sketch_object.kind else {
710            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
711                "Object is not a sketch, it is {}",
712                sketch_object.kind.human_friendly_kind_with_article()
713            ))));
714        };
715        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
716
717        // Enter sketch mode by setting the sketch_mode.
718        self.scene_graph.sketch_mode = Some(sketch);
719
720        // Truncate after the sketch block for mock execution.
721        let mut truncated_program = self.program.clone();
722        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
723            .map_err(KclErrorWithOutputs::no_outputs)?;
724
725        // Execute in mock mode to ensure state is up to date. The caller will
726        // want freedom analysis to display segments correctly.
727        let outcome = ctx
728            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
729            .await?;
730
731        // MockConfig::default() has freedom_analysis: true
732        let outcome = self.update_state_after_exec(outcome, true);
733        let scene_graph_delta = SceneGraphDelta {
734            new_graph: self.scene_graph_for_ui(),
735            invalidates_ids: false,
736            new_objects: Vec::new(),
737            exec_outcome: outcome,
738        };
739        Ok(scene_graph_delta)
740    }
741
742    async fn exit_sketch(
743        &mut self,
744        ctx: &ExecutorContext,
745        _version: Version,
746        sketch: ObjectId,
747    ) -> ExecResult<SceneGraph> {
748        // TODO: Check version.
749        #[cfg(not(target_arch = "wasm32"))]
750        let _ = sketch;
751        #[cfg(target_arch = "wasm32")]
752        if self.scene_graph.sketch_mode != Some(sketch) {
753            web_sys::console::warn_1(
754                &format!(
755                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
756                    &self.scene_graph.sketch_mode
757                )
758                .into(),
759            );
760        }
761        self.scene_graph.sketch_mode = None;
762
763        // Execute.
764        let outcome = ctx.run_with_caching(self.program.clone()).await?;
765
766        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
767        self.update_state_after_exec(outcome, false);
768
769        Ok(self.scene_graph_for_ui())
770    }
771
772    async fn delete_sketch(
773        &mut self,
774        ctx: &ExecutorContext,
775        _version: Version,
776        sketch: ObjectId,
777    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
778        // TODO: Check version.
779
780        let mut new_ast = self.program.ast.clone();
781
782        // Look up existing sketch.
783        let sketch_id = sketch;
784        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
785            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
786        })?;
787        let ObjectKind::Sketch(_) = &sketch_object.kind else {
788            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
789                "Object is not a sketch, it is {}",
790                sketch_object.kind.human_friendly_kind_with_article(),
791            ))));
792        };
793
794        // Modify the AST to remove the sketch.
795        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
796            .map_err(KclErrorWithOutputs::no_outputs)?;
797
798        self.execute_after_delete_sketch(ctx, &mut new_ast).await
799    }
800
801    async fn add_segment(
802        &mut self,
803        ctx: &ExecutorContext,
804        _version: Version,
805        sketch: ObjectId,
806        segment: SegmentCtor,
807        _label: Option<String>,
808    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
809        // TODO: Check version.
810        match segment {
811            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
812            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
813            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
814            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
815            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
816        }
817    }
818
819    async fn edit_segments(
820        &mut self,
821        ctx: &ExecutorContext,
822        _version: Version,
823        sketch: ObjectId,
824        segments: Vec<ExistingSegmentCtor>,
825    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
826        // TODO: Check version.
827        let sketch_block_ref =
828            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
829
830        let mut new_ast = self.program.ast.clone();
831        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
832        let mut invalidates_ids = false;
833
834        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
835        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
836        for segment in &segments {
837            edited_segment_ids.insert(segment.id);
838            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
839                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
840                && let ObjectKind::Segment {
841                    segment: Segment::ControlPointSpline(existing_spline),
842                } = &existing_object.kind
843                && existing_spline.controls.len() != new_ctor.points.len()
844            {
845                invalidates_ids = true;
846            }
847        }
848        let drag_anchor_segment_ids = self
849            .next_drag_anchor_segment_ids
850            .take()
851            .unwrap_or_else(|| edited_segment_ids.clone());
852        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
853
854        // Preprocess segments into a final_edits vector to handle if segments contains:
855        // - edit start point of line1 (as SegmentCtor::Point)
856        // - edit end point of line1 (as SegmentCtor::Point)
857        //
858        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
859        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
860        //
861        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
862        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
863        // so the above example would result in a single line1 edit:
864        // - the first start point edit creates a new line edit entry in final_edits
865        // - the second end point edit finds this line edit and mutates the end position only.
866        //
867        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
868        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
869
870        for segment in segments {
871            let segment_id = segment.id;
872            match segment.ctor {
873                SegmentCtor::Point(ctor) => {
874                    // Find the owner, if any (point -> line / arc)
875                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
876                        && let ObjectKind::Segment { segment } = &segment_object.kind
877                        && let Segment::Point(point) = segment
878                        && let Some(owner_id) = point.owner
879                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
880                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
881                    {
882                        match owner_segment {
883                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
884                                if let Some(existing) = final_edits.get_mut(&owner_id) {
885                                    let SegmentCtor::Line(line_ctor) = existing else {
886                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
887                                            "Internal: Expected line ctor for owner, but found {}",
888                                            existing.human_friendly_kind_with_article()
889                                        ))));
890                                    };
891                                    // Line owner is already in final_edits -> apply this point edit
892                                    if line.start == segment_id {
893                                        line_ctor.start = ctor.position;
894                                    } else {
895                                        line_ctor.end = ctor.position;
896                                    }
897                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
898                                    // Line owner is not in final_edits yet -> create it
899                                    let mut line_ctor = line_ctor.clone();
900                                    if line.start == segment_id {
901                                        line_ctor.start = ctor.position;
902                                    } else {
903                                        line_ctor.end = ctor.position;
904                                    }
905                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
906                                } else {
907                                    // This should never run..
908                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
909                                        "Internal: Line does not have line ctor, but found {}",
910                                        line.ctor.human_friendly_kind_with_article()
911                                    ))));
912                                }
913                                continue;
914                            }
915                            Segment::Arc(arc)
916                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
917                            {
918                                if let Some(existing) = final_edits.get_mut(&owner_id) {
919                                    let SegmentCtor::Arc(arc_ctor) = existing else {
920                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
921                                            "Internal: Expected arc ctor for owner, but found {}",
922                                            existing.human_friendly_kind_with_article()
923                                        ))));
924                                    };
925                                    if arc.start == segment_id {
926                                        arc_ctor.start = ctor.position;
927                                    } else if arc.end == segment_id {
928                                        arc_ctor.end = ctor.position;
929                                    } else {
930                                        arc_ctor.center = ctor.position;
931                                    }
932                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
933                                    let mut arc_ctor = arc_ctor.clone();
934                                    if arc.start == segment_id {
935                                        arc_ctor.start = ctor.position;
936                                    } else if arc.end == segment_id {
937                                        arc_ctor.end = ctor.position;
938                                    } else {
939                                        arc_ctor.center = ctor.position;
940                                    }
941                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
942                                } else {
943                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
944                                        "Internal: Arc does not have arc ctor, but found {}",
945                                        arc.ctor.human_friendly_kind_with_article()
946                                    ))));
947                                }
948                                continue;
949                            }
950                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
951                                if let Some(existing) = final_edits.get_mut(&owner_id) {
952                                    let SegmentCtor::Circle(circle_ctor) = existing else {
953                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
954                                            "Internal: Expected circle ctor for owner, but found {}",
955                                            existing.human_friendly_kind_with_article()
956                                        ))));
957                                    };
958                                    if circle.start == segment_id {
959                                        circle_ctor.start = ctor.position;
960                                    } else {
961                                        circle_ctor.center = ctor.position;
962                                    }
963                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
964                                    let mut circle_ctor = circle_ctor.clone();
965                                    if circle.start == segment_id {
966                                        circle_ctor.start = ctor.position;
967                                    } else {
968                                        circle_ctor.center = ctor.position;
969                                    }
970                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
971                                } else {
972                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
973                                        "Internal: Circle does not have circle ctor, but found {}",
974                                        circle.ctor.human_friendly_kind_with_article()
975                                    ))));
976                                }
977                                continue;
978                            }
979                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
980                                let Some(control_index) =
981                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
982                                else {
983                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
984                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
985                                    ))));
986                                };
987                                if let Some(existing) = final_edits.get_mut(&owner_id) {
988                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
989                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
990                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
991                                            existing.human_friendly_kind_with_article()
992                                        ))));
993                                    };
994                                    spline_ctor.points[control_index] = ctor.position;
995                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
996                                    let mut spline_ctor = spline_ctor.clone();
997                                    spline_ctor.points[control_index] = ctor.position;
998                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
999                                } else {
1000                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1001                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1002                                        spline.ctor.human_friendly_kind_with_article()
1003                                    ))));
1004                                }
1005                                continue;
1006                            }
1007                            _ => {}
1008                        }
1009                    }
1010
1011                    // No owner, it's an individual point
1012                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1013                }
1014                SegmentCtor::Line(ctor) => {
1015                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1016                }
1017                SegmentCtor::Arc(ctor) => {
1018                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1019                }
1020                SegmentCtor::Circle(ctor) => {
1021                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1022                }
1023                SegmentCtor::ControlPointSpline(ctor) => {
1024                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1025                }
1026            }
1027        }
1028
1029        for (segment_id, ctor) in final_edits {
1030            match ctor {
1031                SegmentCtor::Point(ctor) => self
1032                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1033                    .map_err(KclErrorWithOutputs::no_outputs)?,
1034                SegmentCtor::Line(ctor) => self
1035                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1036                    .map_err(KclErrorWithOutputs::no_outputs)?,
1037                SegmentCtor::Arc(ctor) => self
1038                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1039                    .map_err(KclErrorWithOutputs::no_outputs)?,
1040                SegmentCtor::Circle(ctor) => self
1041                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1042                    .map_err(KclErrorWithOutputs::no_outputs)?,
1043                SegmentCtor::ControlPointSpline(ctor) => self
1044                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1045                    .map_err(KclErrorWithOutputs::no_outputs)?,
1046            }
1047        }
1048        let (source_delta, mut scene_graph_delta) = self
1049            .execute_after_edit(
1050                ctx,
1051                sketch,
1052                sketch_block_ref,
1053                &mut new_ast,
1054                ExecuteAfterEditOptions {
1055                    segment_ids_edited: drag_anchor_segment_ids,
1056                    edit_kind: EditDeleteKind::Edit,
1057                    commit_solved_initial_guesses,
1058                },
1059            )
1060            .await?;
1061        if invalidates_ids {
1062            scene_graph_delta.invalidates_ids = true;
1063        }
1064        Ok((source_delta, scene_graph_delta))
1065    }
1066
1067    async fn delete_objects(
1068        &mut self,
1069        ctx: &ExecutorContext,
1070        _version: Version,
1071        sketch: ObjectId,
1072        constraint_ids: Vec<ObjectId>,
1073        segment_ids: Vec<ObjectId>,
1074    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1075        // TODO: Check version.
1076        let sketch_block_ref =
1077            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1078
1079        // Deduplicate IDs.
1080        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1081        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1082
1083        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1084        // also delete the point, as well as other points that are owned by the owner.
1085        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1086
1087        for segment_id in segment_ids_set.iter().copied() {
1088            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1089                let ObjectKind::Segment { segment } = &segment_object.kind else {
1090                    return None;
1091                };
1092                match segment {
1093                    Segment::Point(point) => point.owner,
1094                    Segment::Line(line) => line.owner,
1095                    _ => None,
1096                }
1097            });
1098
1099            if let Some(owner_id) = owner_id
1100                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1101                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1102                && matches!(
1103                    owner_segment,
1104                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1105                )
1106            {
1107                // segment is owned -> delete the owner
1108                resolved_segment_ids_to_delete.insert(owner_id);
1109            } else {
1110                // segment is not owned by anything -> can be deleted
1111                resolved_segment_ids_to_delete.insert(segment_id);
1112            }
1113        }
1114        let referenced_constraint_ids = self
1115            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1116            .map_err(KclErrorWithOutputs::no_outputs)?;
1117
1118        let mut new_ast = self.program.ast.clone();
1119
1120        for constraint_id in referenced_constraint_ids {
1121            if constraint_ids_set.contains(&constraint_id) {
1122                continue;
1123            }
1124
1125            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1126                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1127            })?;
1128            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1129                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1130                    "Object is not a constraint, it is {}",
1131                    constraint_object.kind.human_friendly_kind_with_article()
1132                ))));
1133            };
1134
1135            match constraint {
1136                Constraint::Coincident(coincident) => {
1137                    let remaining_segments =
1138                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1139
1140                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1141                    if remaining_segments.len() >= 2 {
1142                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1143                            .map_err(KclErrorWithOutputs::no_outputs)?;
1144                    } else {
1145                        constraint_ids_set.insert(constraint_id);
1146                    }
1147                }
1148                Constraint::EqualRadius(equal_radius) => {
1149                    let remaining_input = equal_radius
1150                        .input
1151                        .iter()
1152                        .copied()
1153                        .filter(|segment_id| {
1154                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1155                        })
1156                        .collect::<Vec<_>>();
1157
1158                    if remaining_input.len() >= 2 {
1159                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1160                            .map_err(KclErrorWithOutputs::no_outputs)?;
1161                    } else {
1162                        constraint_ids_set.insert(constraint_id);
1163                    }
1164                }
1165                Constraint::LinesEqualLength(lines_equal_length) => {
1166                    let remaining_lines = lines_equal_length
1167                        .lines
1168                        .iter()
1169                        .copied()
1170                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1171                        .collect::<Vec<_>>();
1172
1173                    // Equal length constraint is only valid with at least 2 lines
1174                    if remaining_lines.len() >= 2 {
1175                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1176                            .map_err(KclErrorWithOutputs::no_outputs)?;
1177                    } else {
1178                        constraint_ids_set.insert(constraint_id);
1179                    }
1180                }
1181                Constraint::Parallel(parallel) => {
1182                    let remaining_lines = parallel
1183                        .lines
1184                        .iter()
1185                        .copied()
1186                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1187                        .collect::<Vec<_>>();
1188
1189                    if remaining_lines.len() >= 2 {
1190                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1191                            .map_err(KclErrorWithOutputs::no_outputs)?;
1192                    } else {
1193                        constraint_ids_set.insert(constraint_id);
1194                    }
1195                }
1196                Constraint::Horizontal(Horizontal::Points { points }) => {
1197                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1198
1199                    if remaining_points.len() >= 2 {
1200                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1201                            .map_err(KclErrorWithOutputs::no_outputs)?;
1202                    } else {
1203                        constraint_ids_set.insert(constraint_id);
1204                    }
1205                }
1206                Constraint::Vertical(Vertical::Points { points }) => {
1207                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1208
1209                    if remaining_points.len() >= 2 {
1210                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1211                            .map_err(KclErrorWithOutputs::no_outputs)?;
1212                    } else {
1213                        constraint_ids_set.insert(constraint_id);
1214                    }
1215                }
1216                Constraint::Fixed(fixed) => {
1217                    if fixed.points.iter().any(|fixed_point| {
1218                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1219                    }) {
1220                        constraint_ids_set.insert(constraint_id);
1221                    }
1222                }
1223                _ => {
1224                    // All other constraint types: if referenced by a segment -> delete the constraint
1225                    constraint_ids_set.insert(constraint_id);
1226                }
1227            }
1228        }
1229
1230        for constraint_id in constraint_ids_set {
1231            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1232                .map_err(KclErrorWithOutputs::no_outputs)?;
1233        }
1234        for segment_id in resolved_segment_ids_to_delete {
1235            self.delete_segment(&mut new_ast, sketch, segment_id)
1236                .map_err(KclErrorWithOutputs::no_outputs)?;
1237        }
1238
1239        self.execute_after_edit(
1240            ctx,
1241            sketch,
1242            sketch_block_ref,
1243            &mut new_ast,
1244            ExecuteAfterEditOptions {
1245                segment_ids_edited: Default::default(),
1246                edit_kind: EditDeleteKind::DeleteNonSketch,
1247                commit_solved_initial_guesses: true,
1248            },
1249        )
1250        .await
1251    }
1252
1253    async fn add_constraint(
1254        &mut self,
1255        ctx: &ExecutorContext,
1256        _version: Version,
1257        sketch: ObjectId,
1258        constraint: Constraint,
1259    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1260        // TODO: Check version.
1261
1262        // Save the original state as a backup - we'll restore it if anything fails
1263        let original_program = self.program.clone();
1264        let original_scene_graph = self.scene_graph.clone();
1265
1266        let mut new_ast = self.program.ast.clone();
1267        let sketch_block_ref = match constraint {
1268            Constraint::Coincident(coincident) => self
1269                .add_coincident(sketch, coincident, &mut new_ast)
1270                .await
1271                .map_err(KclErrorWithOutputs::no_outputs)?,
1272            Constraint::Distance(distance) => self
1273                .add_distance(sketch, distance, &mut new_ast)
1274                .await
1275                .map_err(KclErrorWithOutputs::no_outputs)?,
1276            Constraint::EqualRadius(equal_radius) => self
1277                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1278                .await
1279                .map_err(KclErrorWithOutputs::no_outputs)?,
1280            Constraint::Fixed(fixed) => self
1281                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1282                .await
1283                .map_err(KclErrorWithOutputs::no_outputs)?,
1284            Constraint::HorizontalDistance(distance) => self
1285                .add_horizontal_distance(sketch, distance, &mut new_ast)
1286                .await
1287                .map_err(KclErrorWithOutputs::no_outputs)?,
1288            Constraint::VerticalDistance(distance) => self
1289                .add_vertical_distance(sketch, distance, &mut new_ast)
1290                .await
1291                .map_err(KclErrorWithOutputs::no_outputs)?,
1292            Constraint::Horizontal(horizontal) => self
1293                .add_horizontal(sketch, horizontal, &mut new_ast)
1294                .await
1295                .map_err(KclErrorWithOutputs::no_outputs)?,
1296            Constraint::LinesEqualLength(lines_equal_length) => self
1297                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1298                .await
1299                .map_err(KclErrorWithOutputs::no_outputs)?,
1300            Constraint::Midpoint(midpoint) => self
1301                .add_midpoint(sketch, midpoint, &mut new_ast)
1302                .await
1303                .map_err(KclErrorWithOutputs::no_outputs)?,
1304            Constraint::Parallel(parallel) => self
1305                .add_parallel(sketch, parallel, &mut new_ast)
1306                .await
1307                .map_err(KclErrorWithOutputs::no_outputs)?,
1308            Constraint::Perpendicular(perpendicular) => self
1309                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1310                .await
1311                .map_err(KclErrorWithOutputs::no_outputs)?,
1312            Constraint::Radius(radius) => self
1313                .add_radius(sketch, radius, &mut new_ast)
1314                .await
1315                .map_err(KclErrorWithOutputs::no_outputs)?,
1316            Constraint::Diameter(diameter) => self
1317                .add_diameter(sketch, diameter, &mut new_ast)
1318                .await
1319                .map_err(KclErrorWithOutputs::no_outputs)?,
1320            Constraint::Symmetric(symmetric) => self
1321                .add_symmetric(sketch, symmetric, &mut new_ast)
1322                .await
1323                .map_err(KclErrorWithOutputs::no_outputs)?,
1324            Constraint::Vertical(vertical) => self
1325                .add_vertical(sketch, vertical, &mut new_ast)
1326                .await
1327                .map_err(KclErrorWithOutputs::no_outputs)?,
1328            Constraint::Angle(lines_at_angle) => self
1329                .add_angle(sketch, lines_at_angle, &mut new_ast)
1330                .await
1331                .map_err(KclErrorWithOutputs::no_outputs)?,
1332            Constraint::Tangent(tangent) => self
1333                .add_tangent(sketch, tangent, &mut new_ast)
1334                .await
1335                .map_err(KclErrorWithOutputs::no_outputs)?,
1336        };
1337
1338        let result = self
1339            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1340            .await;
1341
1342        // If execution failed, restore the original state to prevent corruption
1343        if result.is_err() {
1344            self.program = original_program;
1345            self.scene_graph = original_scene_graph;
1346        }
1347
1348        result
1349    }
1350
1351    async fn chain_segment(
1352        &mut self,
1353        ctx: &ExecutorContext,
1354        version: Version,
1355        sketch: ObjectId,
1356        previous_segment_end_point_id: ObjectId,
1357        segment: SegmentCtor,
1358        _label: Option<String>,
1359    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1360        // TODO: Check version.
1361
1362        // First, add the segment (line) to get its start point ID
1363        let SegmentCtor::Line(line_ctor) = segment else {
1364            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1365                "chain_segment currently only supports Line segments, got {}",
1366                segment.human_friendly_kind_with_article(),
1367            ))));
1368        };
1369
1370        // Add the line segment first - this updates self.program and self.scene_graph
1371        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1372
1373        // Find the new line's start point ID from the updated scene graph
1374        // add_line updates self.scene_graph, so we can use that
1375        let new_line_id = first_scene_delta
1376            .new_objects
1377            .iter()
1378            .find(|&obj_id| {
1379                let obj = self.scene_graph.objects.get(obj_id.0);
1380                if let Some(obj) = obj {
1381                    matches!(
1382                        &obj.kind,
1383                        ObjectKind::Segment {
1384                            segment: Segment::Line(_)
1385                        }
1386                    )
1387                } else {
1388                    false
1389                }
1390            })
1391            .ok_or_else(|| {
1392                KclErrorWithOutputs::no_outputs(KclError::refactor(
1393                    "Failed to find new line segment in scene graph".to_string(),
1394                ))
1395            })?;
1396
1397        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1398            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1399                "New line object not found: {new_line_id:?}"
1400            )))
1401        })?;
1402
1403        let ObjectKind::Segment {
1404            segment: new_line_segment,
1405        } = &new_line_obj.kind
1406        else {
1407            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1408                "Object is not a segment: {new_line_obj:?}"
1409            ))));
1410        };
1411
1412        let Segment::Line(new_line) = new_line_segment else {
1413            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1414                "Segment is not a line: {new_line_segment:?}"
1415            ))));
1416        };
1417
1418        let new_line_start_point_id = new_line.start;
1419
1420        // Now add the coincident constraint between the previous end point and the new line's start point.
1421        let coincident = Coincident {
1422            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1423        };
1424
1425        let (final_src_delta, final_scene_delta) = self
1426            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1427            .await?;
1428
1429        // Combine new objects from the line addition and the constraint addition.
1430        // Both add_line and add_constraint now populate new_objects correctly.
1431        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1432        combined_new_objects.extend(final_scene_delta.new_objects);
1433
1434        let scene_graph_delta = SceneGraphDelta {
1435            new_graph: self.scene_graph_for_ui(),
1436            invalidates_ids: false,
1437            new_objects: combined_new_objects,
1438            exec_outcome: final_scene_delta.exec_outcome,
1439        };
1440
1441        Ok((final_src_delta, scene_graph_delta))
1442    }
1443
1444    async fn edit_constraint(
1445        &mut self,
1446        ctx: &ExecutorContext,
1447        _version: Version,
1448        sketch: ObjectId,
1449        constraint_id: ObjectId,
1450        value_expression: String,
1451    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1452        // TODO: Check version.
1453        let sketch_block_ref =
1454            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1455
1456        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1457            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1458        })?;
1459        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1460            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1461                "Object is not a constraint: {constraint_id:?}"
1462            ))));
1463        }
1464
1465        let mut new_ast = self.program.ast.clone();
1466
1467        // Parse the expression string into an AST node.
1468        let (parsed, errors) = Program::parse(&value_expression)
1469            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1470        if !errors.is_empty() {
1471            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1472                "Error parsing value expression: {errors:?}"
1473            ))));
1474        }
1475        let mut parsed = parsed.ok_or_else(|| {
1476            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1477        })?;
1478        if parsed.ast.body.is_empty() {
1479            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1480                "Empty value expression".to_string(),
1481            )));
1482        }
1483        let first = parsed.ast.body.remove(0);
1484        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1485            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1486                "Value expression must be a simple expression".to_string(),
1487            )));
1488        };
1489
1490        let new_value: ast::BinaryPart = expr_stmt
1491            .inner
1492            .expression
1493            .try_into()
1494            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1495
1496        self.mutate_ast(
1497            &mut new_ast,
1498            constraint_id,
1499            AstMutateCommand::EditConstraintValue { value: new_value },
1500        )
1501        .map_err(KclErrorWithOutputs::no_outputs)?;
1502
1503        self.execute_after_edit(
1504            ctx,
1505            sketch,
1506            sketch_block_ref,
1507            &mut new_ast,
1508            ExecuteAfterEditOptions {
1509                segment_ids_edited: Default::default(),
1510                edit_kind: EditDeleteKind::Edit,
1511                commit_solved_initial_guesses: true,
1512            },
1513        )
1514        .await
1515    }
1516
1517    async fn edit_distance_constraint_label_position(
1518        &mut self,
1519        ctx: &ExecutorContext,
1520        _version: Version,
1521        sketch: ObjectId,
1522        constraint_id: ObjectId,
1523        label_position: Point2d<Number>,
1524        anchor_segment_ids: Vec<ObjectId>,
1525    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1526        // TODO: Check version.
1527        let sketch_block_ref =
1528            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1529
1530        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1531            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1532        })?;
1533        if !matches!(
1534            &object.kind,
1535            ObjectKind::Constraint {
1536                constraint: Constraint::Distance(_)
1537                    | Constraint::HorizontalDistance(_)
1538                    | Constraint::VerticalDistance(_)
1539                    | Constraint::Radius(_)
1540                    | Constraint::Diameter(_),
1541            }
1542        ) {
1543            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "Object does not support labelPosition: {constraint_id:?}"
1545            ))));
1546        }
1547
1548        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1549            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1550                "Could not convert label position to AST: {err}"
1551            )))
1552        })?;
1553        let mut new_ast = self.program.ast.clone();
1554        self.mutate_ast(
1555            &mut new_ast,
1556            constraint_id,
1557            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1558        )
1559        .map_err(KclErrorWithOutputs::no_outputs)?;
1560        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1561
1562        self.execute_after_edit(
1563            ctx,
1564            sketch,
1565            sketch_block_ref,
1566            &mut new_ast,
1567            ExecuteAfterEditOptions {
1568                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1569                edit_kind: EditDeleteKind::Edit,
1570                commit_solved_initial_guesses,
1571            },
1572        )
1573        .await
1574    }
1575
1576    /// Splitting a segment means creating a new segment, editing the old one, and then
1577    /// migrating a bunch of the constraints from the original segment to the new one
1578    /// (i.e. deleting them and re-adding them on the other segment).
1579    ///
1580    /// To keep this efficient we require as few executions as possible: we create the
1581    /// new segment first (to get its id), then do all edits and new constraints, and
1582    /// do all deletes at the end (since deletes invalidate ids).
1583    async fn batch_split_segment_operations(
1584        &mut self,
1585        ctx: &ExecutorContext,
1586        _version: Version,
1587        sketch: ObjectId,
1588        edit_segments: Vec<ExistingSegmentCtor>,
1589        add_constraints: Vec<Constraint>,
1590        delete_constraint_ids: Vec<ObjectId>,
1591        _new_segment_info: sketch::NewSegmentInfo,
1592    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1593        // TODO: Check version.
1594        let sketch_block_ref =
1595            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1596
1597        let mut new_ast = self.program.ast.clone();
1598        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1599
1600        // Step 1: Edit segments
1601        for segment in edit_segments {
1602            segment_ids_edited.insert(segment.id);
1603            match segment.ctor {
1604                SegmentCtor::Point(ctor) => self
1605                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1606                    .map_err(KclErrorWithOutputs::no_outputs)?,
1607                SegmentCtor::Line(ctor) => self
1608                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1609                    .map_err(KclErrorWithOutputs::no_outputs)?,
1610                SegmentCtor::Arc(ctor) => self
1611                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1612                    .map_err(KclErrorWithOutputs::no_outputs)?,
1613                SegmentCtor::Circle(ctor) => self
1614                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1615                    .map_err(KclErrorWithOutputs::no_outputs)?,
1616                SegmentCtor::ControlPointSpline(ctor) => self
1617                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1618                    .map_err(KclErrorWithOutputs::no_outputs)?,
1619            }
1620        }
1621
1622        // Step 2: Add all constraints
1623        for constraint in add_constraints {
1624            match constraint {
1625                Constraint::Coincident(coincident) => {
1626                    self.add_coincident(sketch, coincident, &mut new_ast)
1627                        .await
1628                        .map_err(KclErrorWithOutputs::no_outputs)?;
1629                }
1630                Constraint::Distance(distance) => {
1631                    self.add_distance(sketch, distance, &mut new_ast)
1632                        .await
1633                        .map_err(KclErrorWithOutputs::no_outputs)?;
1634                }
1635                Constraint::EqualRadius(equal_radius) => {
1636                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1637                        .await
1638                        .map_err(KclErrorWithOutputs::no_outputs)?;
1639                }
1640                Constraint::Fixed(fixed) => {
1641                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1642                        .await
1643                        .map_err(KclErrorWithOutputs::no_outputs)?;
1644                }
1645                Constraint::HorizontalDistance(distance) => {
1646                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1647                        .await
1648                        .map_err(KclErrorWithOutputs::no_outputs)?;
1649                }
1650                Constraint::VerticalDistance(distance) => {
1651                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1652                        .await
1653                        .map_err(KclErrorWithOutputs::no_outputs)?;
1654                }
1655                Constraint::Horizontal(horizontal) => {
1656                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1657                        .await
1658                        .map_err(KclErrorWithOutputs::no_outputs)?;
1659                }
1660                Constraint::LinesEqualLength(lines_equal_length) => {
1661                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1662                        .await
1663                        .map_err(KclErrorWithOutputs::no_outputs)?;
1664                }
1665                Constraint::Midpoint(midpoint) => {
1666                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1667                        .await
1668                        .map_err(KclErrorWithOutputs::no_outputs)?;
1669                }
1670                Constraint::Parallel(parallel) => {
1671                    self.add_parallel(sketch, parallel, &mut new_ast)
1672                        .await
1673                        .map_err(KclErrorWithOutputs::no_outputs)?;
1674                }
1675                Constraint::Perpendicular(perpendicular) => {
1676                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1677                        .await
1678                        .map_err(KclErrorWithOutputs::no_outputs)?;
1679                }
1680                Constraint::Vertical(vertical) => {
1681                    self.add_vertical(sketch, vertical, &mut new_ast)
1682                        .await
1683                        .map_err(KclErrorWithOutputs::no_outputs)?;
1684                }
1685                Constraint::Diameter(diameter) => {
1686                    self.add_diameter(sketch, diameter, &mut new_ast)
1687                        .await
1688                        .map_err(KclErrorWithOutputs::no_outputs)?;
1689                }
1690                Constraint::Radius(radius) => {
1691                    self.add_radius(sketch, radius, &mut new_ast)
1692                        .await
1693                        .map_err(KclErrorWithOutputs::no_outputs)?;
1694                }
1695                Constraint::Symmetric(symmetric) => {
1696                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1697                        .await
1698                        .map_err(KclErrorWithOutputs::no_outputs)?;
1699                }
1700                Constraint::Angle(angle) => {
1701                    self.add_angle(sketch, angle, &mut new_ast)
1702                        .await
1703                        .map_err(KclErrorWithOutputs::no_outputs)?;
1704                }
1705                Constraint::Tangent(tangent) => {
1706                    self.add_tangent(sketch, tangent, &mut new_ast)
1707                        .await
1708                        .map_err(KclErrorWithOutputs::no_outputs)?;
1709                }
1710            }
1711        }
1712
1713        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1714        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1715
1716        let has_constraint_deletions = !constraint_ids_set.is_empty();
1717        for constraint_id in constraint_ids_set {
1718            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1719                .map_err(KclErrorWithOutputs::no_outputs)?;
1720        }
1721
1722        // Step 4: Execute once at the end
1723        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1724        // But we'll manually set invalidates_ids: true if we deleted constraints
1725        let (source_delta, mut scene_graph_delta) = self
1726            .execute_after_edit(
1727                ctx,
1728                sketch,
1729                sketch_block_ref,
1730                &mut new_ast,
1731                ExecuteAfterEditOptions {
1732                    segment_ids_edited,
1733                    edit_kind: EditDeleteKind::Edit,
1734                    commit_solved_initial_guesses: true,
1735                },
1736            )
1737            .await?;
1738
1739        // If we deleted constraints, set invalidates_ids: true
1740        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1741        if has_constraint_deletions {
1742            scene_graph_delta.invalidates_ids = true;
1743        }
1744
1745        Ok((source_delta, scene_graph_delta))
1746    }
1747
1748    async fn batch_tail_cut_operations(
1749        &mut self,
1750        ctx: &ExecutorContext,
1751        _version: Version,
1752        sketch: ObjectId,
1753        edit_segments: Vec<ExistingSegmentCtor>,
1754        add_constraints: Vec<Constraint>,
1755        delete_constraint_ids: Vec<ObjectId>,
1756        additional_edited_segment_ids: Vec<ObjectId>,
1757    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1758        let sketch_block_ref =
1759            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1760
1761        let mut new_ast = self.program.ast.clone();
1762        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1763
1764        // Step 1: Edit segments (usually a single segment for tail cut)
1765        for segment in edit_segments {
1766            segment_ids_edited.insert(segment.id);
1767            match segment.ctor {
1768                SegmentCtor::Point(ctor) => self
1769                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1770                    .map_err(KclErrorWithOutputs::no_outputs)?,
1771                SegmentCtor::Line(ctor) => self
1772                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1773                    .map_err(KclErrorWithOutputs::no_outputs)?,
1774                SegmentCtor::Arc(ctor) => self
1775                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1776                    .map_err(KclErrorWithOutputs::no_outputs)?,
1777                SegmentCtor::Circle(ctor) => self
1778                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1779                    .map_err(KclErrorWithOutputs::no_outputs)?,
1780                SegmentCtor::ControlPointSpline(ctor) => self
1781                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1782                    .map_err(KclErrorWithOutputs::no_outputs)?,
1783            }
1784        }
1785
1786        segment_ids_edited.extend(additional_edited_segment_ids);
1787
1788        // Step 2: Add coincident constraints
1789        for constraint in add_constraints {
1790            match constraint {
1791                Constraint::Coincident(coincident) => {
1792                    self.add_coincident(sketch, coincident, &mut new_ast)
1793                        .await
1794                        .map_err(KclErrorWithOutputs::no_outputs)?;
1795                }
1796                other => {
1797                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1798                        "unsupported constraint in tail cut batch: {other:?}"
1799                    ))));
1800                }
1801            }
1802        }
1803
1804        // Step 3: Delete constraints (if any)
1805        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1806
1807        let has_constraint_deletions = !constraint_ids_set.is_empty();
1808        for constraint_id in constraint_ids_set {
1809            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1810                .map_err(KclErrorWithOutputs::no_outputs)?;
1811        }
1812
1813        // Step 4: Single execute_after_edit
1814        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1815        // But we'll manually set invalidates_ids: true if we deleted constraints
1816        let (source_delta, mut scene_graph_delta) = self
1817            .execute_after_edit(
1818                ctx,
1819                sketch,
1820                sketch_block_ref,
1821                &mut new_ast,
1822                ExecuteAfterEditOptions {
1823                    segment_ids_edited,
1824                    edit_kind: EditDeleteKind::Edit,
1825                    commit_solved_initial_guesses: true,
1826                },
1827            )
1828            .await?;
1829
1830        // If we deleted constraints, set invalidates_ids: true
1831        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1832        if has_constraint_deletions {
1833            scene_graph_delta.invalidates_ids = true;
1834        }
1835
1836        Ok((source_delta, scene_graph_delta))
1837    }
1838}
1839
1840impl FrontendState {
1841    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1842        self.program = program.clone();
1843
1844        // Execute so that the objects are updated and available for the next
1845        // API call.
1846        // This always uses engine execution (not mock) so that things are cached.
1847        // Engine execution now runs freedom analysis automatically.
1848        // Keep existing checkpoints alive here. History may still reference
1849        // older committed sketch states across a direct-edit boundary, and a
1850        // checkpoint restore is a full state replacement anyway. We append a
1851        // fresh baseline checkpoint after the full execution below.
1852        // Clear the freedom cache since IDs might have changed after direct editing
1853        // and we're about to run freedom analysis which will repopulate it.
1854        self.point_freedom_cache.clear();
1855        match ctx.run_with_caching(program).await {
1856            Ok(outcome) => {
1857                let outcome = self.update_state_after_exec(outcome, true);
1858                let checkpoint_id = self
1859                    .create_sketch_checkpoint(outcome.clone())
1860                    .await
1861                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1862                Ok(SetProgramOutcome::Success {
1863                    scene_graph: Box::new(self.scene_graph_for_ui()),
1864                    exec_outcome: Box::new(outcome),
1865                    checkpoint_id: Some(checkpoint_id),
1866                })
1867            }
1868            Err(mut err) => {
1869                // Don't return an error just because execution failed. Instead,
1870                // update state as much as possible.
1871                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1872                self.update_state_after_exec(outcome, true);
1873                err.scene_graph = Some(self.scene_graph_for_ui());
1874                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1875            }
1876        }
1877    }
1878
1879    /// Decorate engine execution such that our state is updated and the scene
1880    /// graph is added to the return.
1881    pub async fn engine_execute(
1882        &mut self,
1883        ctx: &ExecutorContext,
1884        program: Program,
1885    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1886        self.program = program.clone();
1887
1888        // Engine execution now runs freedom analysis automatically. Clear the
1889        // freedom cache since IDs might have changed after direct editing, and
1890        // we're about to run freedom analysis which will repopulate it.
1891        self.point_freedom_cache.clear();
1892        match ctx.run_with_caching(program).await {
1893            Ok(outcome) => {
1894                let outcome = self.update_state_after_exec(outcome, true);
1895                Ok(SceneGraphDelta {
1896                    new_graph: self.scene_graph_for_ui(),
1897                    exec_outcome: outcome,
1898                    // We don't know what the new objects are.
1899                    new_objects: Default::default(),
1900                    // We don't know if IDs were invalidated.
1901                    invalidates_ids: Default::default(),
1902                })
1903            }
1904            Err(mut err) => {
1905                // Update state as much as possible, even when there's an error.
1906                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1907                self.update_state_after_exec(outcome, true);
1908                err.scene_graph = Some(self.scene_graph_for_ui());
1909                Err(err)
1910            }
1911        }
1912    }
1913
1914    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1915        if matches!(err.error, KclError::EngineHangup { .. }) {
1916            // It's not ideal to special-case this, but this error is very
1917            // common during development, and it causes confusing downstream
1918            // errors that have nothing to do with the actual problem.
1919            return Err(err);
1920        }
1921
1922        let KclErrorWithOutputs {
1923            error,
1924            mut non_fatal,
1925            variables,
1926            operations,
1927            artifact_graph,
1928            scene_objects,
1929            source_range_to_object,
1930            var_solutions,
1931            filenames,
1932            default_planes,
1933            ..
1934        } = err;
1935
1936        if let Some(source_range) = error.source_ranges().first() {
1937            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1938        } else {
1939            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1940        }
1941
1942        Ok(ExecOutcome {
1943            variables,
1944            filenames,
1945            operations,
1946            artifact_graph,
1947            scene_objects,
1948            source_range_to_object,
1949            var_solutions,
1950            issues: non_fatal,
1951            default_planes,
1952        })
1953    }
1954
1955    async fn add_point(
1956        &mut self,
1957        ctx: &ExecutorContext,
1958        sketch: ObjectId,
1959        ctor: PointCtor,
1960    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1961        // Create updated KCL source from args.
1962        let at_ast = to_ast_point2d(&ctor.position)
1963            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1964        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1965            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1966            unlabeled: None,
1967            arguments: vec![ast::LabeledArg {
1968                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1969                arg: at_ast,
1970            }],
1971            digest: None,
1972            non_code_meta: Default::default(),
1973        })));
1974
1975        // Look up existing sketch.
1976        let sketch_id = sketch;
1977        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1978            #[cfg(target_arch = "wasm32")]
1979            web_sys::console::error_1(
1980                &format!(
1981                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1982                    &self.scene_graph.objects
1983                )
1984                .into(),
1985            );
1986            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1987        })?;
1988        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1989            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1990                "Object is not a sketch, it is {}",
1991                sketch_object.kind.human_friendly_kind_with_article(),
1992            ))));
1993        };
1994        // Add the point to the AST of the sketch block.
1995        let mut new_ast = self.program.ast.clone();
1996        let (sketch_block_ref, _) = self
1997            .mutate_ast(
1998                &mut new_ast,
1999                sketch_id,
2000                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2001            )
2002            .map_err(KclErrorWithOutputs::no_outputs)?;
2003        // Convert to string source to create real source ranges.
2004        let new_source = source_from_ast(&new_ast);
2005        // Parse the new KCL source.
2006        let (new_program, errors) = Program::parse(&new_source)
2007            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2008        if !errors.is_empty() {
2009            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2010                "Error parsing KCL source after adding point: {errors:?}"
2011            ))));
2012        }
2013        let Some(new_program) = new_program else {
2014            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2015                "No AST produced after adding point".to_string(),
2016            )));
2017        };
2018
2019        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2020            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2021                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2022            )))
2023        })?;
2024
2025        // Make sure to only set this if there are no errors.
2026        self.program = new_program.clone();
2027
2028        // Truncate after the sketch block for mock execution.
2029        let mut truncated_program = new_program;
2030        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2031            .map_err(KclErrorWithOutputs::no_outputs)?;
2032
2033        // Execute.
2034        let outcome = ctx
2035            .run_mock(
2036                &truncated_program,
2037                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2038            )
2039            .await?;
2040
2041        let new_object_ids = {
2042            let make_err =
2043                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2044            let segment_id = outcome
2045                .source_range_to_object
2046                .get(&point_node_ref.range)
2047                .copied()
2048                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2049            let segment_object = outcome
2050                .scene_objects
2051                .get(segment_id.0)
2052                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2053            let ObjectKind::Segment { segment } = &segment_object.kind else {
2054                return Err(make_err(format!(
2055                    "Object is not a segment, it is {}",
2056                    segment_object.kind.human_friendly_kind_with_article()
2057                )));
2058            };
2059            let Segment::Point(_) = segment else {
2060                return Err(make_err(format!(
2061                    "Segment is not a point, it is {}",
2062                    segment.human_friendly_kind_with_article()
2063                )));
2064            };
2065            vec![segment_id]
2066        };
2067        let src_delta = SourceDelta { text: new_source };
2068        // Uses .no_freedom_analysis() so freedom_analysis: false
2069        let outcome = self.update_state_after_exec(outcome, false);
2070        let scene_graph_delta = SceneGraphDelta {
2071            new_graph: self.scene_graph_for_ui(),
2072            invalidates_ids: false,
2073            new_objects: new_object_ids,
2074            exec_outcome: outcome,
2075        };
2076        Ok((src_delta, scene_graph_delta))
2077    }
2078
2079    async fn add_line(
2080        &mut self,
2081        ctx: &ExecutorContext,
2082        sketch: ObjectId,
2083        ctor: LineCtor,
2084    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2085        // Create updated KCL source from args.
2086        let start_ast = to_ast_point2d(&ctor.start)
2087            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2088        let end_ast = to_ast_point2d(&ctor.end)
2089            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2090        let mut arguments = vec![
2091            ast::LabeledArg {
2092                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2093                arg: start_ast,
2094            },
2095            ast::LabeledArg {
2096                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2097                arg: end_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 line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2112            callee: ast::Node::no_src(ast_sketch2_name(LINE_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 line 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: line_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 line: {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 line".to_string(),
2152            )));
2153        };
2154
2155        let line_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 line 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(&line_node_ref.range)
2183                .copied()
2184                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2185            let segment_object = outcome
2186                .scene_object_by_id(segment_id)
2187                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2188            let ObjectKind::Segment { segment } = &segment_object.kind else {
2189                return Err(make_err(format!(
2190                    "Object is not a segment, it is {}",
2191                    segment_object.kind.human_friendly_kind_with_article()
2192                )));
2193            };
2194            let Segment::Line(line) = segment else {
2195                return Err(make_err(format!(
2196                    "Segment is not a line, it is {}",
2197                    segment.human_friendly_kind_with_article()
2198                )));
2199            };
2200            vec![line.start, line.end, segment_id]
2201        };
2202        let src_delta = SourceDelta { text: new_source };
2203        // Uses .no_freedom_analysis() so freedom_analysis: false
2204        let outcome = self.update_state_after_exec(outcome, false);
2205        let scene_graph_delta = SceneGraphDelta {
2206            new_graph: self.scene_graph_for_ui(),
2207            invalidates_ids: false,
2208            new_objects: new_object_ids,
2209            exec_outcome: outcome,
2210        };
2211        Ok((src_delta, scene_graph_delta))
2212    }
2213
2214    async fn add_arc(
2215        &mut self,
2216        ctx: &ExecutorContext,
2217        sketch: ObjectId,
2218        ctor: ArcCtor,
2219    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2220        // Create updated KCL source from args.
2221        let start_ast = to_ast_point2d(&ctor.start)
2222            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2223        let end_ast = to_ast_point2d(&ctor.end)
2224            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2225        let center_ast = to_ast_point2d(&ctor.center)
2226            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2227        let mut arguments = vec![
2228            ast::LabeledArg {
2229                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2230                arg: start_ast,
2231            },
2232            ast::LabeledArg {
2233                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2234                arg: end_ast,
2235            },
2236            ast::LabeledArg {
2237                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2238                arg: center_ast,
2239            },
2240        ];
2241        // Add construction kwarg if construction is Some(true)
2242        if ctor.construction == Some(true) {
2243            arguments.push(ast::LabeledArg {
2244                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2245                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2246                    value: ast::LiteralValue::Bool(true),
2247                    raw: "true".to_string(),
2248                    digest: None,
2249                }))),
2250            });
2251        }
2252        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2253            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2254            unlabeled: None,
2255            arguments,
2256            digest: None,
2257            non_code_meta: Default::default(),
2258        })));
2259
2260        // Look up existing sketch.
2261        let sketch_id = sketch;
2262        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2263            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2264        })?;
2265        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2266            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2267                "Object is not a sketch, it is {}",
2268                sketch_object.kind.human_friendly_kind_with_article(),
2269            ))));
2270        };
2271        // Add the arc to the AST of the sketch block.
2272        let mut new_ast = self.program.ast.clone();
2273        let (sketch_block_ref, _) = self
2274            .mutate_ast(
2275                &mut new_ast,
2276                sketch_id,
2277                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2278            )
2279            .map_err(KclErrorWithOutputs::no_outputs)?;
2280        // Convert to string source to create real source ranges.
2281        let new_source = source_from_ast(&new_ast);
2282        // Parse the new KCL source.
2283        let (new_program, errors) = Program::parse(&new_source)
2284            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2285        if !errors.is_empty() {
2286            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2287                "Error parsing KCL source after adding arc: {errors:?}"
2288            ))));
2289        }
2290        let Some(new_program) = new_program else {
2291            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2292                "No AST produced after adding arc".to_string(),
2293            )));
2294        };
2295
2296        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2297            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2298                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2299            )))
2300        })?;
2301
2302        // Make sure to only set this if there are no errors.
2303        self.program = new_program.clone();
2304
2305        // Truncate after the sketch block for mock execution.
2306        let mut truncated_program = new_program;
2307        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2308            .map_err(KclErrorWithOutputs::no_outputs)?;
2309
2310        // Execute.
2311        let outcome = ctx
2312            .run_mock(
2313                &truncated_program,
2314                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2315            )
2316            .await?;
2317
2318        let new_object_ids = {
2319            let make_err =
2320                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2321            let segment_id = outcome
2322                .source_range_to_object
2323                .get(&arc_node_ref.range)
2324                .copied()
2325                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2326            let segment_object = outcome
2327                .scene_objects
2328                .get(segment_id.0)
2329                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2330            let ObjectKind::Segment { segment } = &segment_object.kind else {
2331                return Err(make_err(format!(
2332                    "Object is not a segment, it is {}",
2333                    segment_object.kind.human_friendly_kind_with_article()
2334                )));
2335            };
2336            let Segment::Arc(arc) = segment else {
2337                return Err(make_err(format!(
2338                    "Segment is not an arc, it is {}",
2339                    segment.human_friendly_kind_with_article()
2340                )));
2341            };
2342            vec![arc.start, arc.end, arc.center, segment_id]
2343        };
2344        let src_delta = SourceDelta { text: new_source };
2345        // Uses .no_freedom_analysis() so freedom_analysis: false
2346        let outcome = self.update_state_after_exec(outcome, false);
2347        let scene_graph_delta = SceneGraphDelta {
2348            new_graph: self.scene_graph_for_ui(),
2349            invalidates_ids: false,
2350            new_objects: new_object_ids,
2351            exec_outcome: outcome,
2352        };
2353        Ok((src_delta, scene_graph_delta))
2354    }
2355
2356    async fn add_circle(
2357        &mut self,
2358        ctx: &ExecutorContext,
2359        sketch: ObjectId,
2360        ctor: CircleCtor,
2361    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2362        // Create updated KCL source from args.
2363        let start_ast = to_ast_point2d(&ctor.start)
2364            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2365        let center_ast = to_ast_point2d(&ctor.center)
2366            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2367        let mut arguments = vec![
2368            ast::LabeledArg {
2369                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2370                arg: start_ast,
2371            },
2372            ast::LabeledArg {
2373                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2374                arg: center_ast,
2375            },
2376        ];
2377        // Add construction kwarg if construction is Some(true)
2378        if ctor.construction == Some(true) {
2379            arguments.push(ast::LabeledArg {
2380                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2381                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2382                    value: ast::LiteralValue::Bool(true),
2383                    raw: "true".to_string(),
2384                    digest: None,
2385                }))),
2386            });
2387        }
2388        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2389            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2390            unlabeled: None,
2391            arguments,
2392            digest: None,
2393            non_code_meta: Default::default(),
2394        })));
2395
2396        // Look up existing sketch.
2397        let sketch_id = sketch;
2398        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2399            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2400        })?;
2401        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2402            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2403                "Object is not a sketch, it is {}",
2404                sketch_object.kind.human_friendly_kind_with_article(),
2405            ))));
2406        };
2407        // Add the circle to the AST of the sketch block.
2408        let mut new_ast = self.program.ast.clone();
2409        let (sketch_block_ref, _) = self
2410            .mutate_ast(
2411                &mut new_ast,
2412                sketch_id,
2413                AstMutateCommand::AddSketchBlockVarDecl {
2414                    prefix: CIRCLE_VARIABLE.to_owned(),
2415                    expr: circle_ast,
2416                },
2417            )
2418            .map_err(KclErrorWithOutputs::no_outputs)?;
2419        // Convert to string source to create real source ranges.
2420        let new_source = source_from_ast(&new_ast);
2421        // Parse the new KCL source.
2422        let (new_program, errors) = Program::parse(&new_source)
2423            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2424        if !errors.is_empty() {
2425            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2426                "Error parsing KCL source after adding circle: {errors:?}"
2427            ))));
2428        }
2429        let Some(new_program) = new_program else {
2430            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2431                "No AST produced after adding circle".to_string(),
2432            )));
2433        };
2434
2435        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2436            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2437                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2438            )))
2439        })?;
2440
2441        // Make sure to only set this if there are no errors.
2442        self.program = new_program.clone();
2443
2444        // Truncate after the sketch block for mock execution.
2445        let mut truncated_program = new_program;
2446        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2447            .map_err(KclErrorWithOutputs::no_outputs)?;
2448
2449        // Execute.
2450        let outcome = ctx
2451            .run_mock(
2452                &truncated_program,
2453                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2454            )
2455            .await?;
2456
2457        let new_object_ids = {
2458            let make_err =
2459                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2460            let segment_id = outcome
2461                .source_range_to_object
2462                .get(&circle_node_ref.range)
2463                .copied()
2464                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2465            let segment_object = outcome
2466                .scene_objects
2467                .get(segment_id.0)
2468                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2469            let ObjectKind::Segment { segment } = &segment_object.kind else {
2470                return Err(make_err(format!(
2471                    "Object is not a segment, it is {}",
2472                    segment_object.kind.human_friendly_kind_with_article()
2473                )));
2474            };
2475            let Segment::Circle(circle) = segment else {
2476                return Err(make_err(format!(
2477                    "Segment is not a circle, it is {}",
2478                    segment.human_friendly_kind_with_article()
2479                )));
2480            };
2481            vec![circle.start, circle.center, segment_id]
2482        };
2483        let src_delta = SourceDelta { text: new_source };
2484        // Uses .no_freedom_analysis() so freedom_analysis: false
2485        let outcome = self.update_state_after_exec(outcome, false);
2486        let scene_graph_delta = SceneGraphDelta {
2487            new_graph: self.scene_graph_for_ui(),
2488            invalidates_ids: false,
2489            new_objects: new_object_ids,
2490            exec_outcome: outcome,
2491        };
2492        Ok((src_delta, scene_graph_delta))
2493    }
2494
2495    async fn add_control_point_spline(
2496        &mut self,
2497        ctx: &ExecutorContext,
2498        sketch: ObjectId,
2499        ctor: ControlPointSplineCtor,
2500    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2501        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2502            .map_err(KclErrorWithOutputs::no_outputs)?;
2503
2504        let points_ast = to_ast_point2d_array(&ctor.points)
2505            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2506        let mut arguments = vec![ast::LabeledArg {
2507            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2508            arg: points_ast,
2509        }];
2510        if ctor.construction == Some(true) {
2511            arguments.push(ast::LabeledArg {
2512                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2513                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2514                    value: ast::LiteralValue::Bool(true),
2515                    raw: "true".to_string(),
2516                    digest: None,
2517                }))),
2518            });
2519        }
2520        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2521            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2522            unlabeled: None,
2523            arguments,
2524            digest: None,
2525            non_code_meta: Default::default(),
2526        })));
2527
2528        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2529            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2530        })?;
2531        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2532            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2533                "Object is not a sketch, it is {}",
2534                sketch_object.kind.human_friendly_kind_with_article(),
2535            ))));
2536        };
2537
2538        let mut new_ast = new_program.ast.clone();
2539        let (sketch_block_ref, _) = self
2540            .mutate_ast(
2541                &mut new_ast,
2542                sketch,
2543                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2544            )
2545            .map_err(KclErrorWithOutputs::no_outputs)?;
2546        let new_source = source_from_ast(&new_ast);
2547        let (new_program, errors) = Program::parse(&new_source)
2548            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2549        if !errors.is_empty() {
2550            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2551                "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2552            ))));
2553        }
2554        let Some(new_program) = new_program else {
2555            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2556                "No AST produced after adding controlPointSpline".to_string(),
2557            )));
2558        };
2559
2560        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2561            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2562                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2563            )))
2564        })?;
2565
2566        self.program = new_program.clone();
2567
2568        let mut truncated_program = new_program;
2569        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2570            .map_err(KclErrorWithOutputs::no_outputs)?;
2571
2572        let outcome = ctx
2573            .run_mock(
2574                &truncated_program,
2575                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2576            )
2577            .await?;
2578
2579        let new_object_ids = {
2580            let make_err =
2581                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2582            let segment_id = outcome
2583                .source_range_to_object
2584                .get(&spline_node_ref.range)
2585                .copied()
2586                .ok_or_else(|| {
2587                    make_err(format!(
2588                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2589                    ))
2590                })?;
2591            let segment_object = outcome
2592                .scene_objects
2593                .get(segment_id.0)
2594                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2595            let ObjectKind::Segment { segment } = &segment_object.kind else {
2596                return Err(make_err(format!(
2597                    "Object is not a segment, it is {}",
2598                    segment_object.kind.human_friendly_kind_with_article()
2599                )));
2600            };
2601            let Segment::ControlPointSpline(spline) = segment else {
2602                return Err(make_err(format!(
2603                    "Segment is not a control point spline, it is {}",
2604                    segment.human_friendly_kind_with_article()
2605                )));
2606            };
2607
2608            let mut ids = outcome
2609                .scene_objects
2610                .iter()
2611                .filter_map(|obj| match &obj.kind {
2612                    ObjectKind::Segment {
2613                        segment: Segment::Line(line),
2614                    } if line.owner == Some(segment_id) => Some(obj.id),
2615                    _ => None,
2616                })
2617                .collect::<Vec<_>>();
2618            ids.extend(spline.controls.clone());
2619            ids.push(segment_id);
2620            ids
2621        };
2622        let src_delta = SourceDelta { text: new_source };
2623        let outcome = self.update_state_after_exec(outcome, false);
2624        let scene_graph_delta = SceneGraphDelta {
2625            new_graph: self.scene_graph_for_ui(),
2626            invalidates_ids: false,
2627            new_objects: new_object_ids,
2628            exec_outcome: outcome,
2629        };
2630        Ok((src_delta, scene_graph_delta))
2631    }
2632
2633    fn edit_point(
2634        &mut self,
2635        new_ast: &mut ast::Node<ast::Program>,
2636        sketch: ObjectId,
2637        point: ObjectId,
2638        ctor: PointCtor,
2639    ) -> Result<(), KclError> {
2640        // Create updated KCL source from args.
2641        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2642
2643        // Look up existing sketch.
2644        let sketch_id = sketch;
2645        let sketch_object = self
2646            .scene_graph
2647            .objects
2648            .get(sketch_id.0)
2649            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2650        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2651            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2652        };
2653        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2654            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2655        })?;
2656        // Look up existing point.
2657        let point_id = point;
2658        let point_object = self
2659            .scene_graph
2660            .objects
2661            .get(point_id.0)
2662            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2663        let ObjectKind::Segment {
2664            segment: Segment::Point(point),
2665        } = &point_object.kind
2666        else {
2667            return Err(KclError::refactor(format!(
2668                "Object is not a point segment: {point_object:?}"
2669            )));
2670        };
2671
2672        // If the point is part of a line or arc, edit the line/arc instead.
2673        if let Some(owner_id) = point.owner {
2674            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2675                KclError::refactor(format!(
2676                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2677                ))
2678            })?;
2679            let ObjectKind::Segment { segment } = &owner_object.kind else {
2680                return Err(KclError::refactor(format!(
2681                    "Internal: Owner of point is not a segment, but found {}",
2682                    owner_object.kind.human_friendly_kind_with_article()
2683                )));
2684            };
2685
2686            // Handle Line owner
2687            if let Segment::Line(line) = segment {
2688                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2689                    return Err(KclError::refactor(format!(
2690                        "Internal: Owner of point does not have line ctor, but found {}",
2691                        line.ctor.human_friendly_kind_with_article()
2692                    )));
2693                };
2694                let mut line_ctor = line_ctor.clone();
2695                // Which end of the line is this point?
2696                if line.start == point_id {
2697                    line_ctor.start = ctor.position;
2698                } else if line.end == point_id {
2699                    line_ctor.end = ctor.position;
2700                } else {
2701                    return Err(KclError::refactor(format!(
2702                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2703                    )));
2704                }
2705                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2706            }
2707
2708            // Handle Arc owner
2709            if let Segment::Arc(arc) = segment {
2710                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2711                    return Err(KclError::refactor(format!(
2712                        "Internal: Owner of point does not have arc ctor, but found {}",
2713                        arc.ctor.human_friendly_kind_with_article()
2714                    )));
2715                };
2716                let mut arc_ctor = arc_ctor.clone();
2717                // Which point of the arc is this? (center, start, or end)
2718                if arc.center == point_id {
2719                    arc_ctor.center = ctor.position;
2720                } else if arc.start == point_id {
2721                    arc_ctor.start = ctor.position;
2722                } else if arc.end == point_id {
2723                    arc_ctor.end = ctor.position;
2724                } else {
2725                    return Err(KclError::refactor(format!(
2726                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2727                    )));
2728                }
2729                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2730            }
2731
2732            // Handle Circle owner
2733            if let Segment::Circle(circle) = segment {
2734                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2735                    return Err(KclError::refactor(format!(
2736                        "Internal: Owner of point does not have circle ctor, but found {}",
2737                        circle.ctor.human_friendly_kind_with_article()
2738                    )));
2739                };
2740                let mut circle_ctor = circle_ctor.clone();
2741                if circle.center == point_id {
2742                    circle_ctor.center = ctor.position;
2743                } else if circle.start == point_id {
2744                    circle_ctor.start = ctor.position;
2745                } else {
2746                    return Err(KclError::refactor(format!(
2747                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2748                    )));
2749                }
2750                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2751            }
2752
2753            if let Segment::ControlPointSpline(spline) = segment {
2754                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2755                    return Err(KclError::refactor(format!(
2756                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2757                        spline.ctor.human_friendly_kind_with_article()
2758                    )));
2759                };
2760                let mut spline_ctor = spline_ctor.clone();
2761                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2762                    return Err(KclError::refactor(format!(
2763                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2764                    )));
2765                };
2766                spline_ctor.points[control_index] = ctor.position;
2767                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2768            }
2769
2770            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2771            // (fall through to the point editing logic below)
2772        }
2773
2774        // Modify the point AST.
2775        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2776        Ok(())
2777    }
2778
2779    fn edit_line(
2780        &mut self,
2781        new_ast: &mut ast::Node<ast::Program>,
2782        sketch: ObjectId,
2783        line: ObjectId,
2784        ctor: LineCtor,
2785    ) -> Result<(), KclError> {
2786        // Create updated KCL source from args.
2787        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2788        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2789
2790        // Look up existing sketch.
2791        let sketch_id = sketch;
2792        let sketch_object = self
2793            .scene_graph
2794            .objects
2795            .get(sketch_id.0)
2796            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2797        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2798            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2799        };
2800        sketch
2801            .segments
2802            .iter()
2803            .find(|o| **o == line)
2804            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2805        // Look up existing line.
2806        let line_id = line;
2807        let line_object = self
2808            .scene_graph
2809            .objects
2810            .get(line_id.0)
2811            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2812        let ObjectKind::Segment { .. } = &line_object.kind else {
2813            let kind = line_object.kind.human_friendly_kind_with_article();
2814            return Err(KclError::refactor(format!(
2815                "This constraint only works on Segments, but you selected {kind}"
2816            )));
2817        };
2818
2819        // Modify the line AST.
2820        self.mutate_ast(
2821            new_ast,
2822            line_id,
2823            AstMutateCommand::EditLine {
2824                start: new_start_ast,
2825                end: new_end_ast,
2826                construction: ctor.construction,
2827            },
2828        )?;
2829        Ok(())
2830    }
2831
2832    fn edit_arc(
2833        &mut self,
2834        new_ast: &mut ast::Node<ast::Program>,
2835        sketch: ObjectId,
2836        arc: ObjectId,
2837        ctor: ArcCtor,
2838    ) -> Result<(), KclError> {
2839        // Create updated KCL source from args.
2840        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2841        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2842        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2843
2844        // Look up existing sketch.
2845        let sketch_id = sketch;
2846        let sketch_object = self
2847            .scene_graph
2848            .objects
2849            .get(sketch_id.0)
2850            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2851        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2852            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2853        };
2854        sketch
2855            .segments
2856            .iter()
2857            .find(|o| **o == arc)
2858            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2859        // Look up existing arc.
2860        let arc_id = arc;
2861        let arc_object = self
2862            .scene_graph
2863            .objects
2864            .get(arc_id.0)
2865            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2866        let ObjectKind::Segment { .. } = &arc_object.kind else {
2867            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2868        };
2869
2870        // Modify the arc AST.
2871        self.mutate_ast(
2872            new_ast,
2873            arc_id,
2874            AstMutateCommand::EditArc {
2875                start: new_start_ast,
2876                end: new_end_ast,
2877                center: new_center_ast,
2878                construction: ctor.construction,
2879            },
2880        )?;
2881        Ok(())
2882    }
2883
2884    fn edit_circle(
2885        &mut self,
2886        new_ast: &mut ast::Node<ast::Program>,
2887        sketch: ObjectId,
2888        circle: ObjectId,
2889        ctor: CircleCtor,
2890    ) -> Result<(), KclError> {
2891        // Create updated KCL source from args.
2892        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2893        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2894
2895        // Look up existing sketch.
2896        let sketch_id = sketch;
2897        let sketch_object = self
2898            .scene_graph
2899            .objects
2900            .get(sketch_id.0)
2901            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2902        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2903            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2904        };
2905        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2906            KclError::refactor(format!(
2907                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2908            ))
2909        })?;
2910        // Look up existing circle.
2911        let circle_id = circle;
2912        let circle_object = self
2913            .scene_graph
2914            .objects
2915            .get(circle_id.0)
2916            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2917        let ObjectKind::Segment { .. } = &circle_object.kind else {
2918            return Err(KclError::refactor(format!(
2919                "Object is not a segment: {circle_object:?}"
2920            )));
2921        };
2922
2923        // Modify the circle AST.
2924        self.mutate_ast(
2925            new_ast,
2926            circle_id,
2927            AstMutateCommand::EditCircle {
2928                start: new_start_ast,
2929                center: new_center_ast,
2930                construction: ctor.construction,
2931            },
2932        )?;
2933        Ok(())
2934    }
2935
2936    fn edit_control_point_spline(
2937        &mut self,
2938        new_ast: &mut ast::Node<ast::Program>,
2939        sketch: ObjectId,
2940        spline: ObjectId,
2941        ctor: ControlPointSplineCtor,
2942    ) -> Result<(), KclError> {
2943        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2944
2945        let sketch_object = self
2946            .scene_graph
2947            .objects
2948            .get(sketch.0)
2949            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2950        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2951            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2952        };
2953        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2954            KclError::refactor(format!(
2955                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2956            ))
2957        })?;
2958
2959        let spline_object =
2960            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2961                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2962            })?;
2963        let ObjectKind::Segment { .. } = &spline_object.kind else {
2964            return Err(KclError::refactor(format!(
2965                "Object is not a segment: {spline_object:?}"
2966            )));
2967        };
2968
2969        self.mutate_ast(
2970            new_ast,
2971            spline,
2972            AstMutateCommand::EditControlPointSpline {
2973                points: points_ast,
2974                construction: ctor.construction,
2975            },
2976        )?;
2977        Ok(())
2978    }
2979
2980    fn delete_segment(
2981        &mut self,
2982        new_ast: &mut ast::Node<ast::Program>,
2983        sketch: ObjectId,
2984        segment_id: ObjectId,
2985    ) -> Result<(), KclError> {
2986        // Look up existing sketch.
2987        let sketch_id = sketch;
2988        let sketch_object = self
2989            .scene_graph
2990            .objects
2991            .get(sketch_id.0)
2992            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2993        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2994            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2995        };
2996        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2997            KclError::refactor(format!(
2998                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2999            ))
3000        })?;
3001        // Look up existing segment.
3002        let segment_object =
3003            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3004                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3005            })?;
3006        let ObjectKind::Segment { .. } = &segment_object.kind else {
3007            return Err(KclError::refactor(format!(
3008                "Object is not a segment, it is {}",
3009                segment_object.kind.human_friendly_kind_with_article()
3010            )));
3011        };
3012
3013        // Modify the AST to remove the segment.
3014        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3015        Ok(())
3016    }
3017
3018    fn delete_constraint(
3019        &mut self,
3020        new_ast: &mut ast::Node<ast::Program>,
3021        sketch: ObjectId,
3022        constraint_id: ObjectId,
3023    ) -> Result<(), KclError> {
3024        // Look up existing sketch.
3025        let sketch_id = sketch;
3026        let sketch_object = self
3027            .scene_graph
3028            .objects
3029            .get(sketch_id.0)
3030            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3031        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3032            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3033        };
3034        sketch
3035            .constraints
3036            .iter()
3037            .find(|o| **o == constraint_id)
3038            .ok_or_else(|| {
3039                KclError::refactor(format!(
3040                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3041                ))
3042            })?;
3043        // Look up existing constraint.
3044        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3045            KclError::refactor(format!(
3046                "Constraint not found in scene graph: constraint={constraint_id:?}"
3047            ))
3048        })?;
3049        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3050            return Err(KclError::refactor(format!(
3051                "Object is not a constraint, it is {}",
3052                constraint_object.kind.human_friendly_kind_with_article()
3053            )));
3054        };
3055
3056        // Modify the AST to remove the constraint.
3057        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3058        Ok(())
3059    }
3060
3061    fn edit_coincident_constraint(
3062        &mut self,
3063        new_ast: &mut ast::Node<ast::Program>,
3064        constraint_id: ObjectId,
3065        segments: Vec<ConstraintSegment>,
3066    ) -> Result<(), KclError> {
3067        if segments.len() < 2 {
3068            return Err(KclError::refactor(format!(
3069                "Coincident constraint must have at least 2 inputs, got {}",
3070                segments.len()
3071            )));
3072        }
3073
3074        let segment_asts = segments
3075            .iter()
3076            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3077            .collect::<Result<Vec<_>, _>>()?;
3078
3079        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3080            elements: segment_asts,
3081            digest: None,
3082            non_code_meta: Default::default(),
3083        })));
3084
3085        self.mutate_ast(
3086            new_ast,
3087            constraint_id,
3088            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3089        )?;
3090        Ok(())
3091    }
3092
3093    fn edit_horizontal_points_constraint(
3094        &mut self,
3095        new_ast: &mut ast::Node<ast::Program>,
3096        constraint_id: ObjectId,
3097        points: Vec<ConstraintSegment>,
3098    ) -> Result<(), KclError> {
3099        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3100    }
3101
3102    fn edit_vertical_points_constraint(
3103        &mut self,
3104        new_ast: &mut ast::Node<ast::Program>,
3105        constraint_id: ObjectId,
3106        points: Vec<ConstraintSegment>,
3107    ) -> Result<(), KclError> {
3108        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3109    }
3110
3111    fn edit_axis_points_constraint(
3112        &mut self,
3113        new_ast: &mut ast::Node<ast::Program>,
3114        constraint_id: ObjectId,
3115        points: Vec<ConstraintSegment>,
3116        constraint_name: &str,
3117    ) -> Result<(), KclError> {
3118        if points.len() < 2 {
3119            return Err(KclError::refactor(format!(
3120                "{constraint_name} points constraint must have at least 2 points, got {}",
3121                points.len()
3122            )));
3123        }
3124
3125        let point_asts = points
3126            .iter()
3127            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3128            .collect::<Result<Vec<_>, _>>()?;
3129
3130        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3131            elements: point_asts,
3132            digest: None,
3133            non_code_meta: Default::default(),
3134        })));
3135
3136        self.mutate_ast(
3137            new_ast,
3138            constraint_id,
3139            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3140        )?;
3141        Ok(())
3142    }
3143
3144    /// updates the equalLength constraint with the given lines
3145    fn edit_equal_length_constraint(
3146        &mut self,
3147        new_ast: &mut ast::Node<ast::Program>,
3148        constraint_id: ObjectId,
3149        lines: Vec<ObjectId>,
3150    ) -> Result<(), KclError> {
3151        if lines.len() < 2 {
3152            return Err(KclError::refactor(format!(
3153                "Lines equal length constraint must have at least 2 lines, got {}",
3154                lines.len()
3155            )));
3156        }
3157
3158        let line_asts = lines
3159            .iter()
3160            .map(|line_id| {
3161                let line_object = self
3162                    .scene_graph
3163                    .objects
3164                    .get(line_id.0)
3165                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3166                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3167                    let kind = line_object.kind.human_friendly_kind_with_article();
3168                    return Err(KclError::refactor(format!(
3169                        "This constraint only works on Segments, but you selected {kind}"
3170                    )));
3171                };
3172                let Segment::Line(_) = line_segment else {
3173                    let kind = line_segment.human_friendly_kind_with_article();
3174                    return Err(KclError::refactor(format!(
3175                        "Only lines can be made equal length, but you selected {kind}"
3176                    )));
3177                };
3178
3179                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3180            })
3181            .collect::<Result<Vec<_>, _>>()?;
3182
3183        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3184            elements: line_asts,
3185            digest: None,
3186            non_code_meta: Default::default(),
3187        })));
3188
3189        self.mutate_ast(
3190            new_ast,
3191            constraint_id,
3192            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3193        )?;
3194        Ok(())
3195    }
3196
3197    /// Updates the parallel constraint with the given lines.
3198    fn edit_parallel_constraint(
3199        &mut self,
3200        new_ast: &mut ast::Node<ast::Program>,
3201        constraint_id: ObjectId,
3202        lines: Vec<ObjectId>,
3203    ) -> Result<(), KclError> {
3204        if lines.len() < 2 {
3205            return Err(KclError::refactor(format!(
3206                "Parallel constraint must have at least 2 lines, got {}",
3207                lines.len()
3208            )));
3209        }
3210
3211        let line_asts = lines
3212            .iter()
3213            .map(|line_id| {
3214                let line_object = self
3215                    .scene_graph
3216                    .objects
3217                    .get(line_id.0)
3218                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3219                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3220                    let kind = line_object.kind.human_friendly_kind_with_article();
3221                    return Err(KclError::refactor(format!(
3222                        "This constraint only works on Segments, but you selected {kind}"
3223                    )));
3224                };
3225                let Segment::Line(_) = line_segment else {
3226                    let kind = line_segment.human_friendly_kind_with_article();
3227                    return Err(KclError::refactor(format!(
3228                        "Only lines can be made parallel, but you selected {kind}"
3229                    )));
3230                };
3231
3232                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3233            })
3234            .collect::<Result<Vec<_>, _>>()?;
3235
3236        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3237            elements: line_asts,
3238            digest: None,
3239            non_code_meta: Default::default(),
3240        })));
3241
3242        self.mutate_ast(
3243            new_ast,
3244            constraint_id,
3245            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3246        )?;
3247        Ok(())
3248    }
3249
3250    /// Updates the equalRadius constraint with the given segments.
3251    fn edit_equal_radius_constraint(
3252        &mut self,
3253        new_ast: &mut ast::Node<ast::Program>,
3254        constraint_id: ObjectId,
3255        input: Vec<ObjectId>,
3256    ) -> Result<(), KclError> {
3257        if input.len() < 2 {
3258            return Err(KclError::refactor(format!(
3259                "equalRadius constraint must have at least 2 segments, got {}",
3260                input.len()
3261            )));
3262        }
3263
3264        let input_asts = input
3265            .iter()
3266            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3267            .collect::<Result<Vec<_>, _>>()?;
3268
3269        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3270            elements: input_asts,
3271            digest: None,
3272            non_code_meta: Default::default(),
3273        })));
3274
3275        self.mutate_ast(
3276            new_ast,
3277            constraint_id,
3278            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3279        )?;
3280        Ok(())
3281    }
3282
3283    async fn execute_after_edit(
3284        &mut self,
3285        ctx: &ExecutorContext,
3286        sketch: ObjectId,
3287        sketch_block_ref: AstNodeRef,
3288        new_ast: &mut ast::Node<ast::Program>,
3289        options: ExecuteAfterEditOptions,
3290    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3291        let ExecuteAfterEditOptions {
3292            segment_ids_edited,
3293            edit_kind,
3294            commit_solved_initial_guesses,
3295        } = options;
3296
3297        // Convert to string source to create real source ranges.
3298        let new_source = source_from_ast(new_ast);
3299        // Parse the new KCL source.
3300        let (new_program, errors) = Program::parse(&new_source)
3301            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3302        if !errors.is_empty() {
3303            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3304                "Error parsing KCL source after editing: {errors:?}"
3305            ))));
3306        }
3307        let Some(new_program) = new_program else {
3308            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3309                "No AST produced after editing".to_string(),
3310            )));
3311        };
3312
3313        // TODO: sketch-api: make sure to only set this if there are no errors.
3314        self.program = new_program.clone();
3315
3316        // Truncate after the sketch block for mock execution.
3317        let is_delete = edit_kind.is_delete();
3318        let truncated_program = {
3319            let mut truncated_program = new_program;
3320            only_sketch_block(
3321                &mut truncated_program.ast,
3322                &sketch_block_ref,
3323                edit_kind.to_change_kind(),
3324            )
3325            .map_err(KclErrorWithOutputs::no_outputs)?;
3326            truncated_program
3327        };
3328
3329        // Execute.
3330        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3331        let mock_config = MockConfig {
3332            sketch_block_id: Some(sketch),
3333            freedom_analysis: is_delete,
3334            segment_ids_edited: segment_ids_edited.clone(),
3335            drag_anchors,
3336            ..Default::default()
3337        };
3338        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3339
3340        // Uses freedom_analysis: is_delete
3341        let outcome = self.update_state_after_exec(outcome, is_delete);
3342
3343        let src_delta = if commit_solved_initial_guesses {
3344            self.commit_var_solutions_to_program(&outcome, "editing")?
3345        } else {
3346            SourceDelta { text: new_source }
3347        };
3348        let scene_graph_delta = SceneGraphDelta {
3349            new_graph: self.scene_graph_for_ui(),
3350            invalidates_ids: is_delete,
3351            new_objects: Vec::new(),
3352            exec_outcome: outcome,
3353        };
3354        Ok((src_delta, scene_graph_delta))
3355    }
3356
3357    async fn execute_after_delete_sketch(
3358        &mut self,
3359        ctx: &ExecutorContext,
3360        new_ast: &mut ast::Node<ast::Program>,
3361    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3362        // Convert to string source to create real source ranges.
3363        let new_source = source_from_ast(new_ast);
3364        // Parse the new KCL source.
3365        let (new_program, errors) = Program::parse(&new_source)
3366            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3367        if !errors.is_empty() {
3368            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3369                "Error parsing KCL source after editing: {errors:?}"
3370            ))));
3371        }
3372        let Some(new_program) = new_program else {
3373            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3374                "No AST produced after editing".to_string(),
3375            )));
3376        };
3377
3378        // Make sure to only set this if there are no errors.
3379        self.program = new_program.clone();
3380
3381        // We deleted the entire sketch block. It doesn't make sense to truncate
3382        // and execute only the sketch block. We execute the whole program with
3383        // a real engine.
3384
3385        // Execute.
3386        let outcome = ctx.run_with_caching(new_program).await?;
3387        let freedom_analysis_ran = true;
3388
3389        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3390
3391        let src_delta = SourceDelta { text: new_source };
3392        let scene_graph_delta = SceneGraphDelta {
3393            new_graph: self.scene_graph_for_ui(),
3394            invalidates_ids: true,
3395            new_objects: Vec::new(),
3396            exec_outcome: outcome,
3397        };
3398        Ok((src_delta, scene_graph_delta))
3399    }
3400
3401    /// Map a point object id into an AST reference expression for use in
3402    /// constraints. If the point is owned by a segment (line or arc), we
3403    /// reference the appropriate property on that segment (e.g. `line1.start`,
3404    /// `arc1.center`). Otherwise we reference the point directly.
3405    fn point_id_to_ast_reference(
3406        &self,
3407        point_id: ObjectId,
3408        new_ast: &mut ast::Node<ast::Program>,
3409    ) -> Result<ast::Expr, KclError> {
3410        let point_object = self
3411            .scene_graph
3412            .objects
3413            .get(point_id.0)
3414            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3415        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3416            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3417        };
3418        let Segment::Point(point) = point_segment else {
3419            return Err(KclError::refactor(format!(
3420                "Only points are currently supported: {point_object:?}"
3421            )));
3422        };
3423
3424        if let Some(owner_id) = point.owner {
3425            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3426                KclError::refactor(format!(
3427                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3428                ))
3429            })?;
3430            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3431                return Err(KclError::refactor(format!(
3432                    "Owner of point is not a segment, but found {}",
3433                    owner_object.kind.human_friendly_kind_with_article()
3434                )));
3435            };
3436
3437            match owner_segment {
3438                Segment::Line(line) => {
3439                    let property = if line.start == point_id {
3440                        LINE_PROPERTY_START
3441                    } else if line.end == point_id {
3442                        LINE_PROPERTY_END
3443                    } else {
3444                        return Err(KclError::refactor(format!(
3445                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3446                        )));
3447                    };
3448                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3449                }
3450                Segment::Arc(arc) => {
3451                    let property = if arc.start == point_id {
3452                        ARC_PROPERTY_START
3453                    } else if arc.end == point_id {
3454                        ARC_PROPERTY_END
3455                    } else if arc.center == point_id {
3456                        ARC_PROPERTY_CENTER
3457                    } else {
3458                        return Err(KclError::refactor(format!(
3459                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3460                        )));
3461                    };
3462                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3463                }
3464                Segment::Circle(circle) => {
3465                    let property = if circle.start == point_id {
3466                        CIRCLE_PROPERTY_START
3467                    } else if circle.center == point_id {
3468                        CIRCLE_PROPERTY_CENTER
3469                    } else {
3470                        return Err(KclError::refactor(format!(
3471                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3472                        )));
3473                    };
3474                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3475                }
3476                Segment::ControlPointSpline(spline) => {
3477                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3478                        return Err(KclError::refactor(format!(
3479                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3480                        )));
3481                    };
3482                    let owner_expr =
3483                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3484                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3485                    Ok(create_index_expression(controls_expr, index))
3486                }
3487                _ => Err(KclError::refactor(format!(
3488                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3489                ))),
3490            }
3491        } else {
3492            // Standalone point.
3493            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3494        }
3495    }
3496
3497    fn line_id_to_ast_reference(
3498        &self,
3499        line_id: ObjectId,
3500        new_ast: &mut ast::Node<ast::Program>,
3501    ) -> Result<ast::Expr, KclError> {
3502        let line_object = self
3503            .scene_graph
3504            .objects
3505            .get(line_id.0)
3506            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3507        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3508            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3509        };
3510        let Segment::Line(line) = line_segment else {
3511            return Err(KclError::refactor(format!(
3512                "Only lines are currently supported: {line_object:?}"
3513            )));
3514        };
3515
3516        if let Some(owner_id) = line.owner {
3517            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3518                KclError::refactor(format!(
3519                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3520                ))
3521            })?;
3522            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3523                return Err(KclError::refactor(format!(
3524                    "Owner of line is not a segment, but found {}",
3525                    owner_object.kind.human_friendly_kind_with_article()
3526                )));
3527            };
3528
3529            match owner_segment {
3530                Segment::ControlPointSpline(spline) => {
3531                    let Some(index) = spline
3532                        .controls
3533                        .windows(2)
3534                        .position(|window| window[0] == line.start && window[1] == line.end)
3535                    else {
3536                        return Err(KclError::refactor(format!(
3537                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3538                        )));
3539                    };
3540                    let owner_expr =
3541                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3542                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3543                    Ok(create_index_expression(edges_expr, index))
3544                }
3545                _ => Err(KclError::refactor(format!(
3546                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3547                ))),
3548            }
3549        } else {
3550            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3551        }
3552    }
3553
3554    fn coincident_segment_to_ast(
3555        &self,
3556        segment: &ConstraintSegment,
3557        new_ast: &mut ast::Node<ast::Program>,
3558    ) -> Result<ast::Expr, KclError> {
3559        match segment {
3560            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3561            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3562        }
3563    }
3564
3565    fn segment_id_to_constraint_ast_reference(
3566        &self,
3567        segment_id: ObjectId,
3568        new_ast: &mut ast::Node<ast::Program>,
3569    ) -> Result<ast::Expr, KclError> {
3570        let segment_object = self
3571            .scene_graph
3572            .objects
3573            .get(segment_id.0)
3574            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3575        let ObjectKind::Segment { segment } = &segment_object.kind else {
3576            return Err(KclError::refactor(format!(
3577                "Object is not a segment, it is {}",
3578                segment_object.kind.human_friendly_kind_with_article()
3579            )));
3580        };
3581
3582        match segment {
3583            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3584            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3585            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3586            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3587            Segment::ControlPointSpline(_) => {
3588                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3589            }
3590        }
3591    }
3592
3593    fn axis_constraint_segment_to_ast(
3594        &self,
3595        segment: &ConstraintSegment,
3596        new_ast: &mut ast::Node<ast::Program>,
3597    ) -> Result<ast::Expr, KclError> {
3598        match segment {
3599            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3600            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3601        }
3602    }
3603
3604    async fn add_coincident(
3605        &mut self,
3606        sketch: ObjectId,
3607        coincident: Coincident,
3608        new_ast: &mut ast::Node<ast::Program>,
3609    ) -> Result<AstNodeRef, KclError> {
3610        let sketch_id = sketch;
3611        for segment in &coincident.segments {
3612            let ConstraintSegment::Segment(segment_id) = segment else {
3613                continue;
3614            };
3615            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3616                continue;
3617            };
3618            if matches!(
3619                segment_object.kind,
3620                ObjectKind::Segment {
3621                    segment: Segment::ControlPointSpline(_)
3622                }
3623            ) {
3624                return Err(KclError::refactor(
3625                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3626                        .to_owned(),
3627                ));
3628            }
3629        }
3630        let segment_asts = coincident
3631            .segments
3632            .iter()
3633            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3634            .collect::<Result<Vec<_>, _>>()?;
3635        if segment_asts.len() < 2 {
3636            return Err(KclError::refactor(format!(
3637                "Coincident constraint must have at least 2 inputs, got {}",
3638                segment_asts.len()
3639            )));
3640        }
3641
3642        // Create the coincident() call using shared helper.
3643        let coincident_ast = create_coincident_ast(segment_asts);
3644
3645        // Add the line to the AST of the sketch block.
3646        let (sketch_block_ref, _) = self.mutate_ast(
3647            new_ast,
3648            sketch_id,
3649            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3650        )?;
3651        Ok(sketch_block_ref)
3652    }
3653
3654    async fn add_distance(
3655        &mut self,
3656        sketch: ObjectId,
3657        distance: Distance,
3658        new_ast: &mut ast::Node<ast::Program>,
3659    ) -> Result<AstNodeRef, KclError> {
3660        let sketch_id = sketch;
3661        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3662            [pt0, pt1] => [
3663                self.coincident_segment_to_ast(pt0, new_ast)?,
3664                self.coincident_segment_to_ast(pt1, new_ast)?,
3665            ],
3666            _ => {
3667                return Err(KclError::refactor(format!(
3668                    "Distance constraint must have exactly 2 points, got {}",
3669                    distance.points.len()
3670                )));
3671            }
3672        };
3673
3674        let arguments = match &distance.label_position {
3675            Some(label_position) => vec![ast::LabeledArg {
3676                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3677                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3678            }],
3679            None => Default::default(),
3680        };
3681
3682        // Create the distance() call.
3683        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3684            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3685            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3686                ast::ArrayExpression {
3687                    elements: vec![pt0_ast, pt1_ast],
3688                    digest: None,
3689                    non_code_meta: Default::default(),
3690                },
3691            )))),
3692            arguments,
3693            digest: None,
3694            non_code_meta: Default::default(),
3695        })));
3696        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3697            left: distance_call_ast,
3698            operator: ast::BinaryOperator::Eq,
3699            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3700                value: ast::LiteralValue::Number {
3701                    value: distance.distance.value,
3702                    suffix: distance.distance.units,
3703                },
3704                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3705                    KclError::refactor(format!(
3706                        "Could not format numeric suffix: {:?}",
3707                        distance.distance.units
3708                    ))
3709                })?,
3710                digest: None,
3711            }))),
3712            digest: None,
3713        })));
3714
3715        // Add the line to the AST of the sketch block.
3716        let (sketch_block_ref, _) = self.mutate_ast(
3717            new_ast,
3718            sketch_id,
3719            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3720        )?;
3721        Ok(sketch_block_ref)
3722    }
3723
3724    async fn add_angle(
3725        &mut self,
3726        sketch: ObjectId,
3727        angle: Angle,
3728        new_ast: &mut ast::Node<ast::Program>,
3729    ) -> Result<AstNodeRef, KclError> {
3730        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3731            return Err(KclError::refactor(format!(
3732                "Angle constraint must have exactly 2 lines, got {}",
3733                angle.lines.len()
3734            )));
3735        };
3736        let sketch_id = sketch;
3737
3738        // Map the runtime objects back to variable names.
3739        let line0_object = self
3740            .scene_graph
3741            .objects
3742            .get(l0_id.0)
3743            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3744        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3745            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3746        };
3747        let Segment::Line(_) = line0_segment else {
3748            return Err(KclError::refactor(format!(
3749                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3750            )));
3751        };
3752        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3753
3754        let line1_object = self
3755            .scene_graph
3756            .objects
3757            .get(l1_id.0)
3758            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3759        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3760            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3761        };
3762        let Segment::Line(_) = line1_segment else {
3763            return Err(KclError::refactor(format!(
3764                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3765            )));
3766        };
3767        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3768
3769        // Create the angle() call.
3770        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3771            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3772            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3773                ast::ArrayExpression {
3774                    elements: vec![l0_ast, l1_ast],
3775                    digest: None,
3776                    non_code_meta: Default::default(),
3777                },
3778            )))),
3779            arguments: Default::default(),
3780            digest: None,
3781            non_code_meta: Default::default(),
3782        })));
3783        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3784            left: angle_call_ast,
3785            operator: ast::BinaryOperator::Eq,
3786            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3787                value: ast::LiteralValue::Number {
3788                    value: angle.angle.value,
3789                    suffix: angle.angle.units,
3790                },
3791                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3792                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3793                })?,
3794                digest: None,
3795            }))),
3796            digest: None,
3797        })));
3798
3799        // Add the line to the AST of the sketch block.
3800        let (sketch_block_ref, _) = self.mutate_ast(
3801            new_ast,
3802            sketch_id,
3803            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3804        )?;
3805        Ok(sketch_block_ref)
3806    }
3807
3808    async fn add_tangent(
3809        &mut self,
3810        sketch: ObjectId,
3811        tangent: Tangent,
3812        new_ast: &mut ast::Node<ast::Program>,
3813    ) -> Result<AstNodeRef, KclError> {
3814        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3815            return Err(KclError::refactor(format!(
3816                "Tangent constraint must have exactly 2 segments, got {}",
3817                tangent.input.len()
3818            )));
3819        };
3820        let sketch_id = sketch;
3821
3822        let seg0_object = self
3823            .scene_graph
3824            .objects
3825            .get(seg0_id.0)
3826            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3827        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3828            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3829        };
3830        let seg0_ast = match seg0_segment {
3831            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3832                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3833            }
3834            _ => {
3835                return Err(KclError::refactor(format!(
3836                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3837                )));
3838            }
3839        };
3840
3841        let seg1_object = self
3842            .scene_graph
3843            .objects
3844            .get(seg1_id.0)
3845            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3846        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3847            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3848        };
3849        let seg1_ast = match seg1_segment {
3850            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3851                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3852            }
3853            _ => {
3854                return Err(KclError::refactor(format!(
3855                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3856                )));
3857            }
3858        };
3859
3860        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3861        let (sketch_block_ref, _) = self.mutate_ast(
3862            new_ast,
3863            sketch_id,
3864            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3865        )?;
3866        Ok(sketch_block_ref)
3867    }
3868
3869    async fn add_symmetric(
3870        &mut self,
3871        sketch: ObjectId,
3872        symmetric: Symmetric,
3873        new_ast: &mut ast::Node<ast::Program>,
3874    ) -> Result<AstNodeRef, KclError> {
3875        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3876            return Err(KclError::refactor(format!(
3877                "Symmetric constraint must have exactly 2 inputs, got {}",
3878                symmetric.input.len()
3879            )));
3880        };
3881        let sketch_id = sketch;
3882
3883        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3884        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3885        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3886
3887        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3888        let (sketch_block_ref, _) = self.mutate_ast(
3889            new_ast,
3890            sketch_id,
3891            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3892        )?;
3893        Ok(sketch_block_ref)
3894    }
3895
3896    async fn add_midpoint(
3897        &mut self,
3898        sketch: ObjectId,
3899        midpoint: Midpoint,
3900        new_ast: &mut ast::Node<ast::Program>,
3901    ) -> Result<AstNodeRef, KclError> {
3902        let sketch_id = sketch;
3903        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3904
3905        let segment_object = self
3906            .scene_graph
3907            .objects
3908            .get(midpoint.segment.0)
3909            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3910        let ObjectKind::Segment {
3911            segment: midpoint_segment,
3912        } = &segment_object.kind
3913        else {
3914            return Err(KclError::refactor(format!(
3915                "Object must be a segment, but it was {}",
3916                segment_object.kind.human_friendly_kind_with_article()
3917            )));
3918        };
3919        let segment_ast = match midpoint_segment {
3920            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3921            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3922            _ => {
3923                return Err(KclError::refactor(format!(
3924                    "Midpoint target must be a line or arc segment but it was {}",
3925                    midpoint_segment.human_friendly_kind_with_article()
3926                )));
3927            }
3928        };
3929
3930        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3931        let (sketch_block_ref, _) = self.mutate_ast(
3932            new_ast,
3933            sketch_id,
3934            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3935        )?;
3936        Ok(sketch_block_ref)
3937    }
3938
3939    async fn add_equal_radius(
3940        &mut self,
3941        sketch: ObjectId,
3942        equal_radius: EqualRadius,
3943        new_ast: &mut ast::Node<ast::Program>,
3944    ) -> Result<AstNodeRef, KclError> {
3945        if equal_radius.input.len() < 2 {
3946            return Err(KclError::refactor(format!(
3947                "equalRadius constraint must have at least 2 segments, got {}",
3948                equal_radius.input.len()
3949            )));
3950        }
3951
3952        let sketch_id = sketch;
3953        let input_asts = equal_radius
3954            .input
3955            .iter()
3956            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3957            .collect::<Result<Vec<_>, _>>()?;
3958
3959        let equal_radius_ast = create_equal_radius_ast(input_asts);
3960        let (sketch_block_ref, _) = self.mutate_ast(
3961            new_ast,
3962            sketch_id,
3963            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3964        )?;
3965        Ok(sketch_block_ref)
3966    }
3967
3968    async fn add_radius(
3969        &mut self,
3970        sketch: ObjectId,
3971        radius: Radius,
3972        new_ast: &mut ast::Node<ast::Program>,
3973    ) -> Result<AstNodeRef, KclError> {
3974        let params = ArcSizeConstraintParams {
3975            points: vec![radius.arc],
3976            function_name: RADIUS_FN,
3977            value: radius.radius.value,
3978            units: radius.radius.units,
3979            label_position: radius.label_position,
3980            constraint_type_name: "Radius",
3981        };
3982        self.add_arc_size_constraint(sketch, params, new_ast).await
3983    }
3984
3985    async fn add_diameter(
3986        &mut self,
3987        sketch: ObjectId,
3988        diameter: Diameter,
3989        new_ast: &mut ast::Node<ast::Program>,
3990    ) -> Result<AstNodeRef, KclError> {
3991        let params = ArcSizeConstraintParams {
3992            points: vec![diameter.arc],
3993            function_name: DIAMETER_FN,
3994            value: diameter.diameter.value,
3995            units: diameter.diameter.units,
3996            label_position: diameter.label_position,
3997            constraint_type_name: "Diameter",
3998        };
3999        self.add_arc_size_constraint(sketch, params, new_ast).await
4000    }
4001
4002    async fn add_fixed_constraints(
4003        &mut self,
4004        sketch: ObjectId,
4005        points: Vec<FixedPoint>,
4006        new_ast: &mut ast::Node<ast::Program>,
4007    ) -> Result<AstNodeRef, KclError> {
4008        let mut sketch_block_ref = None;
4009
4010        for fixed_point in points {
4011            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4012            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4013                .map_err(|err| KclError::refactor(err.to_string()))?;
4014
4015            let (sketch_ref, _) = self.mutate_ast(
4016                new_ast,
4017                sketch,
4018                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4019            )?;
4020            sketch_block_ref = Some(sketch_ref);
4021        }
4022
4023        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4024    }
4025
4026    async fn add_arc_size_constraint(
4027        &mut self,
4028        sketch: ObjectId,
4029        params: ArcSizeConstraintParams,
4030        new_ast: &mut ast::Node<ast::Program>,
4031    ) -> Result<AstNodeRef, KclError> {
4032        let sketch_id = sketch;
4033
4034        // Constraint must have exactly 1 argument (arc segment)
4035        if params.points.len() != 1 {
4036            return Err(KclError::refactor(format!(
4037                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4038                params.constraint_type_name,
4039                params.points.len()
4040            )));
4041        }
4042
4043        let arc_id = params.points[0];
4044        let arc_object = self
4045            .scene_graph
4046            .objects
4047            .get(arc_id.0)
4048            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4049        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4050            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4051        };
4052        let ref_type = match arc_segment {
4053            Segment::Arc(_) => ARC_VARIABLE,
4054            Segment::Circle(_) => CIRCLE_VARIABLE,
4055            _ => {
4056                return Err(KclError::refactor(format!(
4057                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4058                    params.constraint_type_name
4059                )));
4060            }
4061        };
4062        // Reference the arc/circle segment directly
4063        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4064        let arguments = match &params.label_position {
4065            Some(label_position) => vec![ast::LabeledArg {
4066                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4067                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4068            }],
4069            None => Default::default(),
4070        };
4071
4072        // Create the function call.
4073        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4074            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4075            unlabeled: Some(arc_ast),
4076            arguments,
4077            digest: None,
4078            non_code_meta: Default::default(),
4079        })));
4080        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4081            left: call_ast,
4082            operator: ast::BinaryOperator::Eq,
4083            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4084                value: ast::LiteralValue::Number {
4085                    value: params.value,
4086                    suffix: params.units,
4087                },
4088                raw: format_number_literal(params.value, params.units, None)
4089                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4090                digest: None,
4091            }))),
4092            digest: None,
4093        })));
4094
4095        // Add the line to the AST of the sketch block.
4096        let (sketch_block_ref, _) = self.mutate_ast(
4097            new_ast,
4098            sketch_id,
4099            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4100        )?;
4101        Ok(sketch_block_ref)
4102    }
4103
4104    async fn add_horizontal_distance(
4105        &mut self,
4106        sketch: ObjectId,
4107        distance: Distance,
4108        new_ast: &mut ast::Node<ast::Program>,
4109    ) -> Result<AstNodeRef, KclError> {
4110        let sketch_id = sketch;
4111        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4112            [pt0, pt1] => [
4113                self.coincident_segment_to_ast(pt0, new_ast)?,
4114                self.coincident_segment_to_ast(pt1, new_ast)?,
4115            ],
4116            _ => {
4117                return Err(KclError::refactor(format!(
4118                    "Horizontal distance constraint must have exactly 2 points, got {}",
4119                    distance.points.len()
4120                )));
4121            }
4122        };
4123
4124        let arguments = match &distance.label_position {
4125            Some(label_position) => vec![ast::LabeledArg {
4126                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4127                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4128            }],
4129            None => Default::default(),
4130        };
4131
4132        // Create the horizontalDistance() call.
4133        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4134            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4135            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4136                ast::ArrayExpression {
4137                    elements: vec![pt0_ast, pt1_ast],
4138                    digest: None,
4139                    non_code_meta: Default::default(),
4140                },
4141            )))),
4142            arguments,
4143            digest: None,
4144            non_code_meta: Default::default(),
4145        })));
4146        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4147            left: distance_call_ast,
4148            operator: ast::BinaryOperator::Eq,
4149            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4150                value: ast::LiteralValue::Number {
4151                    value: distance.distance.value,
4152                    suffix: distance.distance.units,
4153                },
4154                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4155                    KclError::refactor(format!(
4156                        "Could not format numeric suffix: {:?}",
4157                        distance.distance.units
4158                    ))
4159                })?,
4160                digest: None,
4161            }))),
4162            digest: None,
4163        })));
4164
4165        // Add the line to the AST of the sketch block.
4166        let (sketch_block_ref, _) = self.mutate_ast(
4167            new_ast,
4168            sketch_id,
4169            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4170        )?;
4171        Ok(sketch_block_ref)
4172    }
4173
4174    async fn add_vertical_distance(
4175        &mut self,
4176        sketch: ObjectId,
4177        distance: Distance,
4178        new_ast: &mut ast::Node<ast::Program>,
4179    ) -> Result<AstNodeRef, KclError> {
4180        let sketch_id = sketch;
4181        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4182            [pt0, pt1] => [
4183                self.coincident_segment_to_ast(pt0, new_ast)?,
4184                self.coincident_segment_to_ast(pt1, new_ast)?,
4185            ],
4186            _ => {
4187                return Err(KclError::refactor(format!(
4188                    "Vertical distance constraint must have exactly 2 points, got {}",
4189                    distance.points.len()
4190                )));
4191            }
4192        };
4193
4194        let arguments = match &distance.label_position {
4195            Some(label_position) => vec![ast::LabeledArg {
4196                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4197                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4198            }],
4199            None => Default::default(),
4200        };
4201
4202        // Create the verticalDistance() call.
4203        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4204            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4205            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4206                ast::ArrayExpression {
4207                    elements: vec![pt0_ast, pt1_ast],
4208                    digest: None,
4209                    non_code_meta: Default::default(),
4210                },
4211            )))),
4212            arguments,
4213            digest: None,
4214            non_code_meta: Default::default(),
4215        })));
4216        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4217            left: distance_call_ast,
4218            operator: ast::BinaryOperator::Eq,
4219            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4220                value: ast::LiteralValue::Number {
4221                    value: distance.distance.value,
4222                    suffix: distance.distance.units,
4223                },
4224                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4225                    KclError::refactor(format!(
4226                        "Could not format numeric suffix: {:?}",
4227                        distance.distance.units
4228                    ))
4229                })?,
4230                digest: None,
4231            }))),
4232            digest: None,
4233        })));
4234
4235        // Add the line to the AST of the sketch block.
4236        let (sketch_block_ref, _) = self.mutate_ast(
4237            new_ast,
4238            sketch_id,
4239            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4240        )?;
4241        Ok(sketch_block_ref)
4242    }
4243
4244    async fn add_horizontal(
4245        &mut self,
4246        sketch: ObjectId,
4247        horizontal: Horizontal,
4248        new_ast: &mut ast::Node<ast::Program>,
4249    ) -> Result<AstNodeRef, KclError> {
4250        let sketch_id = sketch;
4251
4252        // Map the runtime objects back to variable names.
4253        let first_arg_ast = match horizontal {
4254            Horizontal::Line { line } => {
4255                let line_object = self
4256                    .scene_graph
4257                    .objects
4258                    .get(line.0)
4259                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4260                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4261                    let kind = line_object.kind.human_friendly_kind_with_article();
4262                    return Err(KclError::refactor(format!(
4263                        "This constraint only works on Segments, but you selected {kind}"
4264                    )));
4265                };
4266                let Segment::Line(_) = line_segment else {
4267                    return Err(KclError::refactor(format!(
4268                        "Only lines can be made horizontal, but you selected {}",
4269                        line_segment.human_friendly_kind_with_article(),
4270                    )));
4271                };
4272                self.line_id_to_ast_reference(line, new_ast)?
4273            }
4274            Horizontal::Points { points } => {
4275                let point_asts = points
4276                    .iter()
4277                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4278                    .collect::<Result<Vec<_>, _>>()?;
4279                ast::ArrayExpression::new(point_asts).into()
4280            }
4281        };
4282        // Create the horizontal() call using shared helper.
4283        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4284
4285        // Add the line to the AST of the sketch block.
4286        let (sketch_block_ref, _) = self.mutate_ast(
4287            new_ast,
4288            sketch_id,
4289            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4290        )?;
4291        Ok(sketch_block_ref)
4292    }
4293
4294    async fn add_lines_equal_length(
4295        &mut self,
4296        sketch: ObjectId,
4297        lines_equal_length: LinesEqualLength,
4298        new_ast: &mut ast::Node<ast::Program>,
4299    ) -> Result<AstNodeRef, KclError> {
4300        if lines_equal_length.lines.len() < 2 {
4301            return Err(KclError::refactor(format!(
4302                "Lines equal length constraint must have at least 2 lines, got {}",
4303                lines_equal_length.lines.len()
4304            )));
4305        };
4306
4307        let sketch_id = sketch;
4308
4309        // Map the runtime objects back to variable names.
4310        let line_asts = lines_equal_length
4311            .lines
4312            .iter()
4313            .map(|line_id| {
4314                let line_object = self
4315                    .scene_graph
4316                    .objects
4317                    .get(line_id.0)
4318                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4319                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4320                    let kind = line_object.kind.human_friendly_kind_with_article();
4321                    return Err(KclError::refactor(format!(
4322                        "This constraint only works on Segments, but you selected {kind}"
4323                    )));
4324                };
4325                let Segment::Line(_) = line_segment else {
4326                    let kind = line_segment.human_friendly_kind_with_article();
4327                    return Err(KclError::refactor(format!(
4328                        "Only lines can be made equal length, but you selected {kind}"
4329                    )));
4330                };
4331
4332                self.line_id_to_ast_reference(*line_id, new_ast)
4333            })
4334            .collect::<Result<Vec<_>, _>>()?;
4335
4336        // Create the equalLength() call using shared helper.
4337        let equal_length_ast = create_equal_length_ast(line_asts);
4338
4339        // Add the constraint to the AST of the sketch block.
4340        let (sketch_block_ref, _) = self.mutate_ast(
4341            new_ast,
4342            sketch_id,
4343            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4344        )?;
4345        Ok(sketch_block_ref)
4346    }
4347
4348    fn equal_radius_segment_id_to_ast_reference(
4349        &mut self,
4350        segment_id: ObjectId,
4351        new_ast: &mut ast::Node<ast::Program>,
4352    ) -> Result<ast::Expr, KclError> {
4353        let segment_object = self
4354            .scene_graph
4355            .objects
4356            .get(segment_id.0)
4357            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4358        let ObjectKind::Segment { segment } = &segment_object.kind else {
4359            return Err(KclError::refactor(format!(
4360                "Object is not a segment, it was {}",
4361                segment_object.kind.human_friendly_kind_with_article()
4362            )));
4363        };
4364
4365        let ref_type = match segment {
4366            Segment::Arc(_) => ARC_VARIABLE,
4367            Segment::Circle(_) => CIRCLE_VARIABLE,
4368            _ => {
4369                return Err(KclError::refactor(format!(
4370                    "equalRadius supports only arc/circle segments, got {}",
4371                    segment.human_friendly_kind_with_article()
4372                )));
4373            }
4374        };
4375
4376        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4377    }
4378
4379    fn symmetric_input_id_to_ast_reference(
4380        &mut self,
4381        segment_id: ObjectId,
4382        new_ast: &mut ast::Node<ast::Program>,
4383    ) -> Result<ast::Expr, KclError> {
4384        let segment_object = self
4385            .scene_graph
4386            .objects
4387            .get(segment_id.0)
4388            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4389        let ObjectKind::Segment { segment } = &segment_object.kind else {
4390            return Err(KclError::refactor(format!(
4391                "Object is not a segment, it was {}",
4392                segment_object.kind.human_friendly_kind_with_article()
4393            )));
4394        };
4395
4396        match segment {
4397            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4398            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4399            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4400            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4401            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4402                "Symmetric does not yet support control point splines".to_owned(),
4403            )),
4404        }
4405    }
4406
4407    fn symmetric_axis_id_to_ast_reference(
4408        &mut self,
4409        segment_id: ObjectId,
4410        new_ast: &mut ast::Node<ast::Program>,
4411    ) -> Result<ast::Expr, KclError> {
4412        let segment_object = self
4413            .scene_graph
4414            .objects
4415            .get(segment_id.0)
4416            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4417        let ObjectKind::Segment { segment } = &segment_object.kind else {
4418            return Err(KclError::refactor(format!(
4419                "Object is not a segment, it was {}",
4420                segment_object.kind.human_friendly_kind_with_article()
4421            )));
4422        };
4423        match segment {
4424            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4425            _ => Err(KclError::refactor(format!(
4426                "Symmetric axis must be a line, got {}",
4427                segment.human_friendly_kind_with_article()
4428            ))),
4429        }
4430    }
4431
4432    async fn add_parallel(
4433        &mut self,
4434        sketch: ObjectId,
4435        parallel: Parallel,
4436        new_ast: &mut ast::Node<ast::Program>,
4437    ) -> Result<AstNodeRef, KclError> {
4438        if parallel.lines.len() < 2 {
4439            return Err(KclError::refactor(format!(
4440                "Parallel constraint must have at least 2 lines, got {}",
4441                parallel.lines.len()
4442            )));
4443        };
4444
4445        let sketch_id = sketch;
4446
4447        let line_asts = parallel
4448            .lines
4449            .iter()
4450            .map(|line_id| {
4451                let line_object = self
4452                    .scene_graph
4453                    .objects
4454                    .get(line_id.0)
4455                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4456                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4457                    let kind = line_object.kind.human_friendly_kind_with_article();
4458                    return Err(KclError::refactor(format!(
4459                        "This constraint only works on Segments, but you selected {kind}"
4460                    )));
4461                };
4462                let Segment::Line(_) = line_segment else {
4463                    let kind = line_segment.human_friendly_kind_with_article();
4464                    return Err(KclError::refactor(format!(
4465                        "Only lines can be made parallel, but you selected {kind}"
4466                    )));
4467                };
4468
4469                self.line_id_to_ast_reference(*line_id, new_ast)
4470            })
4471            .collect::<Result<Vec<_>, _>>()?;
4472
4473        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4474            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4475            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4476                ast::ArrayExpression {
4477                    elements: line_asts,
4478                    digest: None,
4479                    non_code_meta: Default::default(),
4480                },
4481            )))),
4482            arguments: Default::default(),
4483            digest: None,
4484            non_code_meta: Default::default(),
4485        })));
4486
4487        let (sketch_block_ref, _) = self.mutate_ast(
4488            new_ast,
4489            sketch_id,
4490            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4491        )?;
4492        Ok(sketch_block_ref)
4493    }
4494
4495    async fn add_perpendicular(
4496        &mut self,
4497        sketch: ObjectId,
4498        perpendicular: Perpendicular,
4499        new_ast: &mut ast::Node<ast::Program>,
4500    ) -> Result<AstNodeRef, KclError> {
4501        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4502            .await
4503    }
4504
4505    async fn add_lines_at_angle_constraint(
4506        &mut self,
4507        sketch: ObjectId,
4508        angle_kind: LinesAtAngleKind,
4509        lines: Vec<ObjectId>,
4510        new_ast: &mut ast::Node<ast::Program>,
4511    ) -> Result<AstNodeRef, KclError> {
4512        let &[line0_id, line1_id] = lines.as_slice() else {
4513            return Err(KclError::refactor(format!(
4514                "{} constraint must have exactly 2 lines, got {}",
4515                angle_kind.to_function_name(),
4516                lines.len()
4517            )));
4518        };
4519
4520        let sketch_id = sketch;
4521
4522        // Map the runtime objects back to variable names.
4523        let line0_object = self
4524            .scene_graph
4525            .objects
4526            .get(line0_id.0)
4527            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4528        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4529            let kind = line0_object.kind.human_friendly_kind_with_article();
4530            return Err(KclError::refactor(format!(
4531                "This constraint only works on Segments, but you selected {kind}"
4532            )));
4533        };
4534        let Segment::Line(_) = line0_segment else {
4535            return Err(KclError::refactor(format!(
4536                "Only lines can be made {}, but you selected {}",
4537                angle_kind.to_function_name(),
4538                line0_segment.human_friendly_kind_with_article(),
4539            )));
4540        };
4541        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4542
4543        let line1_object = self
4544            .scene_graph
4545            .objects
4546            .get(line1_id.0)
4547            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4548        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4549            let kind = line1_object.kind.human_friendly_kind_with_article();
4550            return Err(KclError::refactor(format!(
4551                "This constraint only works on Segments, but you selected {kind}"
4552            )));
4553        };
4554        let Segment::Line(_) = line1_segment else {
4555            return Err(KclError::refactor(format!(
4556                "Only lines can be made {}, but you selected {}",
4557                angle_kind.to_function_name(),
4558                line1_segment.human_friendly_kind_with_article(),
4559            )));
4560        };
4561        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4562
4563        // Create the parallel() or perpendicular() call.
4564        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4565            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4566            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4567                ast::ArrayExpression {
4568                    elements: vec![line0_ast, line1_ast],
4569                    digest: None,
4570                    non_code_meta: Default::default(),
4571                },
4572            )))),
4573            arguments: Default::default(),
4574            digest: None,
4575            non_code_meta: Default::default(),
4576        })));
4577
4578        // Add the constraint to the AST of the sketch block.
4579        let (sketch_block_ref, _) = self.mutate_ast(
4580            new_ast,
4581            sketch_id,
4582            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4583        )?;
4584        Ok(sketch_block_ref)
4585    }
4586
4587    async fn add_vertical(
4588        &mut self,
4589        sketch: ObjectId,
4590        vertical: Vertical,
4591        new_ast: &mut ast::Node<ast::Program>,
4592    ) -> Result<AstNodeRef, KclError> {
4593        let sketch_id = sketch;
4594
4595        let first_arg_ast = match vertical {
4596            Vertical::Line { line } => {
4597                // Map the runtime objects back to variable names.
4598                let line_object = self
4599                    .scene_graph
4600                    .objects
4601                    .get(line.0)
4602                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4603                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4604                    let kind = line_object.kind.human_friendly_kind_with_article();
4605                    return Err(KclError::refactor(format!(
4606                        "This constraint only works on Segments, but you selected {kind}"
4607                    )));
4608                };
4609                let Segment::Line(_) = line_segment else {
4610                    return Err(KclError::refactor(format!(
4611                        "Only lines can be made vertical, but you selected {}",
4612                        line_segment.human_friendly_kind_with_article()
4613                    )));
4614                };
4615                self.line_id_to_ast_reference(line, new_ast)?
4616            }
4617            Vertical::Points { points } => {
4618                let point_asts = points
4619                    .iter()
4620                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4621                    .collect::<Result<Vec<_>, _>>()?;
4622                ast::ArrayExpression::new(point_asts).into()
4623            }
4624        };
4625        // Create the vertical() call using shared helper.
4626        let vertical_ast = create_vertical_ast(first_arg_ast);
4627
4628        // Add the line to the AST of the sketch block.
4629        let (sketch_block_ref, _) = self.mutate_ast(
4630            new_ast,
4631            sketch_id,
4632            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4633        )?;
4634        Ok(sketch_block_ref)
4635    }
4636
4637    async fn execute_after_add_constraint(
4638        &mut self,
4639        ctx: &ExecutorContext,
4640        sketch_id: ObjectId,
4641        sketch_block_ref: AstNodeRef,
4642        new_ast: &mut ast::Node<ast::Program>,
4643    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4644        // Convert to string source to create real source ranges.
4645        let new_source = source_from_ast(new_ast);
4646        // Parse the new KCL source.
4647        let (new_program, errors) = Program::parse(&new_source)
4648            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4649        if !errors.is_empty() {
4650            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4651                "Error parsing KCL source after adding constraint: {errors:?}"
4652            ))));
4653        }
4654        let Some(new_program) = new_program else {
4655            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4656                "No AST produced after adding constraint".to_string(),
4657            )));
4658        };
4659        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4660            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4661                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4662            )))
4663        })?;
4664
4665        // Truncate after the sketch block for mock execution.
4666        // Use a clone so we don't mutate new_program yet
4667        let mut truncated_program = new_program.clone();
4668        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4669            .map_err(KclErrorWithOutputs::no_outputs)?;
4670
4671        // Execute - if this fails, we haven't modified self yet, so state is safe
4672        let outcome = ctx
4673            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4674            .await?;
4675
4676        let new_object_ids = {
4677            // Extract the constraint ID from the execution outcome using source_range_to_object
4678            let constraint_id = outcome
4679                .source_range_to_object
4680                .get(&constraint_node_ref.range)
4681                .copied()
4682                .ok_or_else(|| {
4683                    KclErrorWithOutputs::from_error_outcome(
4684                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4685                        outcome.clone(),
4686                    )
4687                })?;
4688            vec![constraint_id]
4689        };
4690
4691        // Only now, after all operations succeeded, update self.program.
4692        // This ensures state is only modified if everything succeeds.
4693        self.program = new_program;
4694
4695        // Uses MockConfig::default() which has freedom_analysis: true
4696        let outcome = self.update_state_after_exec(outcome, true);
4697
4698        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4699        let scene_graph_delta = SceneGraphDelta {
4700            new_graph: self.scene_graph_for_ui(),
4701            invalidates_ids: false,
4702            new_objects: new_object_ids,
4703            exec_outcome: outcome,
4704        };
4705        Ok((src_delta, scene_graph_delta))
4706    }
4707
4708    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4709        let commit_failure = || {
4710            KclErrorWithOutputs::from_error_outcome(
4711                KclError::refactor(format!("Could not update KCL after {operation}.")),
4712                outcome.clone(),
4713            )
4714        };
4715
4716        let default_length_unit = self.default_length_unit();
4717        let mut settled_ast = self.program.ast.clone();
4718        let mut committed_solver_value = false;
4719        for (var_range, node_path, value) in &outcome.var_solutions {
4720            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4721                return Err(commit_failure());
4722            };
4723            let new_value = match &lookup {
4724                Some(current_literal) => {
4725                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4726                        continue;
4727                    }
4728                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4729                }
4730                None => {
4731                    // Bare `var` with no initial literal to compare against;
4732                    // always commit, using the module's default length unit as
4733                    // an explicit suffix so the written value carries units.
4734                    Number {
4735                        value: number_value_in_default_length_units(*value, default_length_unit),
4736                        units: default_length_unit.into(),
4737                    }
4738                }
4739            };
4740            committed_solver_value = true;
4741            let source_ref = SourceRef::Simple {
4742                range: *var_range,
4743                node_path: node_path.clone(),
4744            };
4745            mutate_ast_node_by_source_ref(
4746                &mut settled_ast,
4747                &source_ref,
4748                AstMutateCommand::EditVarInitialValue { value: new_value },
4749            )
4750            .map_err(|_| commit_failure())?;
4751        }
4752
4753        if !committed_solver_value {
4754            return Ok(SourceDelta {
4755                text: self.program.original_file_contents.clone(),
4756            });
4757        }
4758
4759        let settled_source = source_from_ast(&settled_ast);
4760        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4761        if !errors.is_empty() {
4762            return Err(commit_failure());
4763        }
4764        let Some(settled_program) = settled_program else {
4765            return Err(commit_failure());
4766        };
4767
4768        self.program = settled_program;
4769
4770        Ok(SourceDelta { text: settled_source })
4771    }
4772
4773    // Find constraints that reference the given segments.
4774    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4775        if segment_ids_set.contains(&segment_id) {
4776            return true;
4777        }
4778
4779        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4780            return false;
4781        };
4782        let ObjectKind::Segment { segment } = &segment_object.kind else {
4783            return false;
4784        };
4785        let Segment::Point(point) = segment else {
4786            return false;
4787        };
4788
4789        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4790    }
4791
4792    fn remaining_constraint_segments(
4793        &self,
4794        segments: &[ConstraintSegment],
4795        segment_ids_set: &AhashIndexSet<ObjectId>,
4796    ) -> Vec<ConstraintSegment> {
4797        segments
4798            .iter()
4799            .copied()
4800            .filter(|segment| match segment {
4801                ConstraintSegment::Origin(_) => true,
4802                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4803            })
4804            .collect()
4805    }
4806
4807    fn find_referenced_constraints(
4808        &self,
4809        sketch_id: ObjectId,
4810        segment_ids_set: &AhashIndexSet<ObjectId>,
4811    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4812        // Look up the sketch.
4813        let sketch_object = self
4814            .scene_graph
4815            .objects
4816            .get(sketch_id.0)
4817            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4818        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4819            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4820        };
4821        let segment_or_owner_matches = |segment_id: ObjectId| {
4822            if segment_ids_set.contains(&segment_id) {
4823                return true;
4824            }
4825            let segment_object = self.scene_graph.objects.get(segment_id.0);
4826            if let Some(obj) = segment_object
4827                && let ObjectKind::Segment { segment } = &obj.kind
4828            {
4829                match segment {
4830                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4831                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4832                    _ => false,
4833                }
4834            } else {
4835                false
4836            }
4837        };
4838        let mut constraint_ids_set = AhashIndexSet::default();
4839        for constraint_id in &sketch.constraints {
4840            let constraint_object = self
4841                .scene_graph
4842                .objects
4843                .get(constraint_id.0)
4844                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4845            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4846                return Err(KclError::refactor(format!(
4847                    "Object is not a constraint, it is {}",
4848                    constraint_object.kind.human_friendly_kind_with_article()
4849                )));
4850            };
4851            let depends_on_segment = match constraint {
4852                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4853                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4854                Constraint::Fixed(fixed) => fixed
4855                    .points
4856                    .iter()
4857                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4858                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4859                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4860                Constraint::EqualRadius(equal_radius) => {
4861                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4862                }
4863                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4864                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4865                Constraint::Horizontal(h) => match h {
4866                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4867                    Horizontal::Points { points } => points.iter().any(|point| match point {
4868                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4869                        ConstraintSegment::Origin(_) => false,
4870                    }),
4871                },
4872                Constraint::Vertical(v) => match v {
4873                    Vertical::Line { line } => segment_or_owner_matches(*line),
4874                    Vertical::Points { points } => points.iter().any(|point| match point {
4875                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4876                        ConstraintSegment::Origin(_) => false,
4877                    }),
4878                },
4879                Constraint::LinesEqualLength(lines_equal_length) => {
4880                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4881                }
4882                Constraint::Midpoint(midpoint) => {
4883                    segment_or_owner_matches(midpoint.segment)
4884                        || matches!(
4885                            midpoint.point,
4886                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4887                        )
4888                }
4889                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4890                Constraint::Perpendicular(perpendicular) => {
4891                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4892                }
4893                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4894                Constraint::Symmetric(symmetric) => {
4895                    segment_or_owner_matches(symmetric.axis)
4896                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4897                }
4898                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4899            };
4900            if depends_on_segment {
4901                constraint_ids_set.insert(*constraint_id);
4902            }
4903        }
4904        Ok(constraint_ids_set)
4905    }
4906
4907    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4908        let mut outcome = outcome;
4909        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4910
4911        if freedom_analysis_ran {
4912            // When freedom analysis ran, replace the cache entirely with new values
4913            // Don't merge with old values since IDs might have changed
4914            self.point_freedom_cache.clear();
4915            for new_obj in &new_objects {
4916                if let ObjectKind::Segment {
4917                    segment: crate::front::Segment::Point(point),
4918                } = &new_obj.kind
4919                {
4920                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4921                }
4922            }
4923            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4924            // Objects are already correct from the analysis, just use them as-is
4925            self.scene_graph.objects = new_objects;
4926        } else {
4927            // When freedom analysis didn't run, preserve old values and merge
4928            // Before replacing objects, extract and store freedom values from old objects
4929            for old_obj in &self.scene_graph.objects {
4930                if let ObjectKind::Segment {
4931                    segment: crate::front::Segment::Point(point),
4932                } = &old_obj.kind
4933                {
4934                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4935                }
4936            }
4937
4938            // Update objects, preserving stored freedom values when new is Free (might be default)
4939            let mut updated_objects = Vec::with_capacity(new_objects.len());
4940            for new_obj in new_objects {
4941                let mut obj = new_obj;
4942                if let ObjectKind::Segment {
4943                    segment: crate::front::Segment::Point(point),
4944                } = &mut obj.kind
4945                {
4946                    let new_freedom = point.freedom;
4947                    // When freedom_analysis=false, new values are defaults (Free).
4948                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4949                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4950                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4951                    // when there are actually unsatisfied constraints.
4952                    match new_freedom {
4953                        Freedom::Free => {
4954                            match self.point_freedom_cache.get(&obj.id).copied() {
4955                                Some(Freedom::Conflict) => {
4956                                    // Don't preserve Conflict - conflicts are transient
4957                                    // Keep it as Free
4958                                }
4959                                Some(Freedom::Fixed) => {
4960                                    // Preserve Fixed cached value
4961                                    point.freedom = Freedom::Fixed;
4962                                }
4963                                Some(Freedom::Free) => {
4964                                    // If stored is also Free, keep Free (no change needed)
4965                                }
4966                                None => {
4967                                    // If no cached value, keep Free (default)
4968                                }
4969                            }
4970                        }
4971                        Freedom::Fixed => {
4972                            // Use new value (already set)
4973                        }
4974                        Freedom::Conflict => {
4975                            // Use new value (already set)
4976                        }
4977                    }
4978                    // Store the new freedom value (even if it's Free, so we know it was set)
4979                    self.point_freedom_cache.insert(obj.id, point.freedom);
4980                }
4981                updated_objects.push(obj);
4982            }
4983
4984            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4985            self.scene_graph.objects = updated_objects;
4986        }
4987        outcome
4988    }
4989
4990    fn mutate_ast(
4991        &mut self,
4992        ast: &mut ast::Node<ast::Program>,
4993        object_id: ObjectId,
4994        command: AstMutateCommand,
4995    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4996        let sketch_object = self
4997            .scene_graph
4998            .objects
4999            .get(object_id.0)
5000            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5001        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5002    }
5003}
5004
5005fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5006    // Look up existing sketch.
5007    let sketch_object = scene_graph
5008        .objects
5009        .get(sketch_id.0)
5010        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5011    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5012        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5013    };
5014    expect_single_node_ref(sketch_object)
5015}
5016
5017fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5018    match &object.source {
5019        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5020            range: *range,
5021            node_path: node_path.clone(),
5022        }),
5023        SourceRef::BackTrace { ranges } => {
5024            let [range] = ranges.as_slice() else {
5025                return Err(KclError::refactor(format!(
5026                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5027                    ranges.len()
5028                )));
5029            };
5030            Ok(AstNodeRef {
5031                range: range.0,
5032                node_path: range.1.clone(),
5033            })
5034        }
5035    }
5036}
5037
5038/// This is a deprecated fall-back implementation. Prefer
5039/// [`only_sketch_block()`] to avoid reliance on source ranges.
5040fn only_sketch_block_from_range(
5041    ast: &mut ast::Node<ast::Program>,
5042    sketch_block_range: SourceRange,
5043    edit_kind: ChangeKind,
5044) -> Result<(), KclError> {
5045    let r1 = sketch_block_range;
5046    let matches_range = |r2: SourceRange| -> bool {
5047        // We may have added items to the sketch block, so the end may not be an
5048        // exact match.
5049        match edit_kind {
5050            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5051            // For edit, we don't know whether it grew or shrank.
5052            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5053            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5054            // No edit should be an exact match.
5055            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5056        }
5057    };
5058    let mut found = false;
5059    for item in ast.body.iter_mut() {
5060        match item {
5061            ast::BodyItem::ImportStatement(_) => {}
5062            ast::BodyItem::ExpressionStatement(node) => {
5063                if matches_range(SourceRange::from(&*node))
5064                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5065                {
5066                    sketch_block.is_being_edited = true;
5067                    found = true;
5068                    break;
5069                }
5070            }
5071            ast::BodyItem::VariableDeclaration(node) => {
5072                if matches_range(SourceRange::from(&node.declaration.init))
5073                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5074                {
5075                    sketch_block.is_being_edited = true;
5076                    found = true;
5077                    break;
5078                }
5079            }
5080            ast::BodyItem::TypeDeclaration(_) => {}
5081            ast::BodyItem::ReturnStatement(node) => {
5082                if matches_range(SourceRange::from(&node.argument))
5083                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5084                {
5085                    sketch_block.is_being_edited = true;
5086                    found = true;
5087                    break;
5088                }
5089            }
5090        }
5091    }
5092    if !found {
5093        return Err(KclError::refactor(format!(
5094            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5095        )));
5096    }
5097
5098    Ok(())
5099}
5100
5101fn only_sketch_block(
5102    ast: &mut ast::Node<ast::Program>,
5103    sketch_block_ref: &AstNodeRef,
5104    edit_kind: ChangeKind,
5105) -> Result<(), KclError> {
5106    let Some(target_node_path) = &sketch_block_ref.node_path else {
5107        #[cfg(target_arch = "wasm32")]
5108        web_sys::console::warn_1(
5109            &format!(
5110                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5111                &sketch_block_ref
5112            )
5113            .into(),
5114        );
5115        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5116    };
5117    let mut found = false;
5118    for item in ast.body.iter_mut() {
5119        match item {
5120            ast::BodyItem::ImportStatement(_) => {}
5121            ast::BodyItem::ExpressionStatement(node) => {
5122                // Check the statement.
5123                if let Some(node_path) = &node.node_path
5124                    && node_path == target_node_path
5125                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5126                {
5127                    sketch_block.is_being_edited = true;
5128                    found = true;
5129                    break;
5130                }
5131                // Check the expression.
5132                if let Some(node_path) = node.expression.node_path()
5133                    && node_path == target_node_path
5134                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5135                {
5136                    sketch_block.is_being_edited = true;
5137                    found = true;
5138                    break;
5139                }
5140            }
5141            ast::BodyItem::VariableDeclaration(node) => {
5142                if let Some(node_path) = node.declaration.init.node_path()
5143                    && node_path == target_node_path
5144                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5145                {
5146                    sketch_block.is_being_edited = true;
5147                    found = true;
5148                    break;
5149                }
5150            }
5151            ast::BodyItem::TypeDeclaration(_) => {}
5152            ast::BodyItem::ReturnStatement(node) => {
5153                if let Some(node_path) = node.argument.node_path()
5154                    && node_path == target_node_path
5155                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5156                {
5157                    sketch_block.is_being_edited = true;
5158                    found = true;
5159                    break;
5160                }
5161            }
5162        }
5163    }
5164    if !found {
5165        return Err(KclError::refactor(format!(
5166            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5167        )));
5168    }
5169
5170    Ok(())
5171}
5172
5173fn sketch_on_ast_expr(
5174    ast: &mut ast::Node<ast::Program>,
5175    scene_graph: &SceneGraph,
5176    on: &Plane,
5177) -> Result<ast::Expr, KclError> {
5178    match on {
5179        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5180        Plane::Object(object_id) => {
5181            let on_object = scene_graph
5182                .objects
5183                .get(object_id.0)
5184                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5185            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5186                return Ok(face_expr);
5187            }
5188            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5189        }
5190    }
5191}
5192
5193fn sketch_face_of_scene_object_ast_expr(
5194    ast: &mut ast::Node<ast::Program>,
5195    on_object: &crate::front::Object,
5196) -> Result<Option<ast::Expr>, KclError> {
5197    match &on_object.kind {
5198        ObjectKind::Wall(wall) => {
5199            let solid_ref = get_or_insert_ast_reference(
5200                ast,
5201                &source_ref_from_source_ref_range(&wall.source.solid),
5202                "solid",
5203                None,
5204            )?;
5205            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5206                return Err(KclError::refactor(format!(
5207                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5208                    on_object.artifact_id
5209                )));
5210            };
5211            let solid_expr = indexed_solid_expr_for_sweep_output(
5212                ast_name_expr(solid_name_expr.name.name.clone()),
5213                wall.solid_output_index,
5214            );
5215            let sweep_ref = get_or_insert_ast_reference(
5216                ast,
5217                &source_ref_from_source_ref_range(&wall.source.sweep),
5218                "solid",
5219                None,
5220            )?;
5221            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5222                return Err(KclError::refactor(format!(
5223                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5224                    on_object.artifact_id
5225                )));
5226            };
5227            let sweep_name = sweep_name_expr.name.name.clone();
5228            let segment_ref = get_or_insert_ast_reference(
5229                ast,
5230                &source_ref_from_source_ref_range(&wall.source.segment),
5231                LINE_VARIABLE,
5232                None,
5233            )?;
5234
5235            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5236                wall.source
5237                    .path
5238                    .as_ref()
5239                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5240            }) {
5241                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5242                    return Err(KclError::refactor(format!(
5243                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5244                        on_object.artifact_id
5245                    )));
5246                };
5247                create_member_expression(
5248                    create_member_expression(ast_name_expr(region_name), "tags"),
5249                    &segment_name_expr.name.name,
5250                )
5251            } else {
5252                segment_ref
5253            };
5254
5255            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5256        }
5257        ObjectKind::Cap(cap) => {
5258            let solid_ref =
5259                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5260            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5261                return Err(KclError::refactor(format!(
5262                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5263                    on_object.artifact_id
5264                )));
5265            };
5266            let solid_expr = indexed_solid_expr_for_sweep_output(
5267                ast_name_expr(solid_name_expr.name.name.clone()),
5268                cap.solid_output_index,
5269            );
5270            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5271            let face_expr = match cap.kind {
5272                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5273                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5274            };
5275
5276            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5277        }
5278        _ => Ok(None),
5279    }
5280}
5281
5282fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5283    match solid_output_index {
5284        Some(output_index) => create_index_expression(solid_expr, output_index),
5285        None => solid_expr,
5286    }
5287}
5288
5289fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5290    SourceRef::Simple {
5291        range: source.range,
5292        node_path: source.node_path.clone(),
5293    }
5294}
5295
5296fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5297    let source_ref = source_ref_from_source_ref_range(path_source);
5298    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5299    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5300        return None;
5301    };
5302    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5303        return None;
5304    };
5305    if region_call.callee.name.name != "region" {
5306        return None;
5307    }
5308    Some(candidate)
5309}
5310
5311fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5312    let mut current_id = source_id;
5313    let mut current_composite = None;
5314    let mut visited = HashSet::new();
5315
5316    while visited.insert(current_id) {
5317        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5318
5319        let Some(composite_id) = next_composite_id else {
5320            break;
5321        };
5322        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5323            break;
5324        };
5325
5326        current_id = composite.id;
5327        current_composite = Some(composite);
5328
5329        if !composite.consumed {
5330            break;
5331        }
5332    }
5333
5334    current_composite.map(|composite| &composite.code_ref)
5335}
5336
5337fn downstream_composite_id_for_solid_source(
5338    artifact_graph: &ArtifactGraph,
5339    source_id: ArtifactId,
5340) -> Option<ArtifactId> {
5341    // Source is a path, find its solid.
5342    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5343        && let Some(composite_id) = path.composite_solid_id
5344        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5345        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5346    {
5347        return Some(composite_id);
5348    }
5349
5350    // Source is a sweep, find its path -> then find the solid
5351    for artifact in artifact_graph.values() {
5352        if let Artifact::Path(path) = artifact
5353            && path.sweep_id == Some(source_id)
5354            && let Some(composite_id) = path.composite_solid_id
5355            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5356            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5357        {
5358            return Some(composite_id);
5359        }
5360    }
5361
5362    // Source is a solid, find its downstream solid.
5363    artifact_graph.values().find_map(|artifact| {
5364        let Artifact::CompositeSolid(composite) = artifact else {
5365            return None;
5366        };
5367        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5368    })
5369}
5370
5371fn composite_contains_path_input(
5372    solid_ids: &[ArtifactId],
5373    tool_ids: &[ArtifactId],
5374    path_id: ArtifactId,
5375    solid2d_id: Option<ArtifactId>,
5376) -> bool {
5377    composite_contains_input(solid_ids, tool_ids, path_id)
5378        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5379}
5380
5381fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5382    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5383}
5384
5385fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5386    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5387    SourceRefRange {
5388        range: code_ref.range,
5389        node_path,
5390    }
5391}
5392
5393fn solid_output_index_for_sweep(
5394    artifact_graph: &ArtifactGraph,
5395    sweep_id: ArtifactId,
5396    sweep_code_ref: &CodeRef,
5397) -> Option<usize> {
5398    let sibling_sweeps = artifact_graph
5399        .values()
5400        .filter_map(|artifact| match artifact {
5401            Artifact::Sweep(sweep)
5402                if sweep.code_ref.range == sweep_code_ref.range
5403                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5404            {
5405                Some(sweep)
5406            }
5407            _ => None,
5408        })
5409        .collect::<Vec<_>>();
5410
5411    if sibling_sweeps.len() <= 1 {
5412        return None;
5413    }
5414
5415    sibling_sweeps
5416        .iter()
5417        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5418}
5419
5420fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5421    let mut existing_artifact_ids = scene_objects
5422        .iter()
5423        .map(|object| object.artifact_id)
5424        .collect::<HashSet<_>>();
5425
5426    for artifact in artifact_graph.values() {
5427        match artifact {
5428            Artifact::Wall(wall) => {
5429                if existing_artifact_ids.contains(&wall.id) {
5430                    continue;
5431                }
5432
5433                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5434                    Artifact::Segment(segment) => Some(segment),
5435                    _ => None,
5436                }) else {
5437                    continue;
5438                };
5439                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5440                    Artifact::Sweep(sweep) => Some(sweep),
5441                    _ => None,
5442                }) else {
5443                    continue;
5444                };
5445                let source_segment = segment
5446                    .original_seg_id
5447                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5448                    .and_then(|artifact| match artifact {
5449                        Artifact::Segment(segment) => Some(segment),
5450                        _ => None,
5451                    })
5452                    .unwrap_or(segment);
5453                let solid_code_ref =
5454                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5455                let path_code_ref = artifact_graph
5456                    .get(&segment.path_id)
5457                    .or_else(|| artifact_graph.get(&sweep.path_id))
5458                    .and_then(|artifact| match artifact {
5459                        Artifact::Path(path) => Some(&path.code_ref),
5460                        _ => None,
5461                    });
5462                let source = WallSource {
5463                    solid: code_ref_source_ref_range(solid_code_ref),
5464                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5465                    path: path_code_ref.map(code_ref_source_ref_range),
5466                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5467                };
5468                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5469                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5470                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5471                    .flatten();
5472                let object_source = source_ref_from_source_ref_range(&source.solid);
5473                let id = ObjectId(scene_objects.len());
5474                scene_objects.push(crate::front::Object {
5475                    id,
5476                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5477                        id,
5478                        source,
5479                        solid_output_index,
5480                    }),
5481                    label: Default::default(),
5482                    comments: Default::default(),
5483                    artifact_id: wall.id,
5484                    source: object_source,
5485                });
5486                existing_artifact_ids.insert(wall.id);
5487            }
5488            Artifact::Cap(cap) => {
5489                if existing_artifact_ids.contains(&cap.id) {
5490                    continue;
5491                }
5492
5493                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5494                    Artifact::Sweep(sweep) => Some(sweep),
5495                    _ => None,
5496                }) else {
5497                    continue;
5498                };
5499                let id = ObjectId(scene_objects.len());
5500                let kind = match cap.sub_type {
5501                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5502                    CapSubType::End => crate::frontend::api::CapKind::End,
5503                };
5504                let solid_code_ref =
5505                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5506                let source = CapSource {
5507                    solid: code_ref_source_ref_range(solid_code_ref),
5508                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5509                };
5510                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5511                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5512                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5513                    .flatten();
5514                let object_source = source_ref_from_source_ref_range(&source.solid);
5515                scene_objects.push(crate::front::Object {
5516                    id,
5517                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5518                        id,
5519                        kind,
5520                        source,
5521                        solid_output_index,
5522                    }),
5523                    label: Default::default(),
5524                    comments: Default::default(),
5525                    artifact_id: cap.id,
5526                    source: object_source,
5527                });
5528                existing_artifact_ids.insert(cap.id);
5529            }
5530            _ => {}
5531        }
5532    }
5533}
5534
5535fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5536    use crate::engine::PlaneName;
5537
5538    match name {
5539        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5540        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5541        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5542        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5543        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5544        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5545    }
5546}
5547
5548fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5549    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5550        ast::UnaryOperator::Neg,
5551        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5552    )))
5553}
5554
5555fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5556    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5557        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5558        unlabeled: Some(solid_expr),
5559        arguments: vec![ast::LabeledArg {
5560            label: Some(ast::Identifier::new("face")),
5561            arg: face_expr,
5562        }],
5563        digest: None,
5564        non_code_meta: Default::default(),
5565    })))
5566}
5567
5568fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5569    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5570        return None;
5571    };
5572    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5573        return None;
5574    };
5575    if !matches!(
5576        sweep_call.callee.name.name.as_str(),
5577        "extrude" | "revolve" | "sweep" | "loft"
5578    ) {
5579        return None;
5580    }
5581    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5582        return None;
5583    };
5584    let candidate = region_name_expr.name.name.clone();
5585    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5586        return None;
5587    };
5588    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5589        return None;
5590    };
5591    if region_call.callee.name.name != "region" {
5592        return None;
5593    }
5594    Some(candidate)
5595}
5596
5597/// Return the AST expression referencing the variable at the given source ref.
5598/// If no such variable exists, insert a new variable declaration with the given
5599/// prefix.
5600///
5601/// This may return a complex expression referencing properties of the variable
5602/// (e.g., `line1.start`).
5603fn get_or_insert_ast_reference(
5604    ast: &mut ast::Node<ast::Program>,
5605    source_ref: &SourceRef,
5606    prefix: &str,
5607    property: Option<&str>,
5608) -> Result<ast::Expr, KclError> {
5609    let command = AstMutateCommand::AddVariableDeclaration {
5610        prefix: prefix.to_owned(),
5611    };
5612    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5613        Ok((_, ret)) => ret,
5614        Err(err) => {
5615            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5616                AstMutateCommandReturn::Name(var_name)
5617            } else {
5618                return Err(err);
5619            }
5620        }
5621    };
5622    let AstMutateCommandReturn::Name(var_name) = ret else {
5623        return Err(KclError::refactor(
5624            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5625        ));
5626    };
5627    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5628    let Some(property) = property else {
5629        // No property; just return the variable name.
5630        return Ok(var_expr);
5631    };
5632
5633    Ok(create_member_expression(var_expr, property))
5634}
5635
5636fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5637    let source_range = match source_ref {
5638        SourceRef::Simple { range, .. } => *range,
5639        SourceRef::BackTrace { ranges } => {
5640            let [range] = ranges.as_slice() else {
5641                return None;
5642            };
5643            range.0
5644        }
5645    };
5646    ast.body.iter().find_map(|item| {
5647        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5648            return None;
5649        };
5650        let init_range = SourceRange::from(&var_decl.declaration.init);
5651        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5652            && init_range.start() <= source_range.start()
5653            && source_range.end() <= init_range.end();
5654        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5655            && init_range != source_range
5656            && source_is_inside_init
5657        {
5658            return None;
5659        }
5660        source_is_inside_init.then(|| var_decl.name().to_owned())
5661    })
5662}
5663
5664fn mutate_ast_node_by_source_ref(
5665    ast: &mut ast::Node<ast::Program>,
5666    source_ref: &SourceRef,
5667    command: AstMutateCommand,
5668) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5669    let (source_range, node_path) = match source_ref {
5670        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5671        SourceRef::BackTrace { ranges } => {
5672            let [range] = ranges.as_slice() else {
5673                return Err(KclError::refactor(format!(
5674                    "Expected single source ref, got {}; ranges={ranges:#?}",
5675                    ranges.len(),
5676                )));
5677            };
5678            (range.0, range.1.clone())
5679        }
5680    };
5681    let mut context = AstMutateContext {
5682        source_range,
5683        node_path,
5684        command,
5685        defined_names_stack: Default::default(),
5686    };
5687    let control = dfs_mut(ast, &mut context);
5688    match control {
5689        ControlFlow::Continue(_) => Err(KclError::refactor(
5690            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5691        )),
5692        ControlFlow::Break(break_value) => break_value,
5693    }
5694}
5695
5696#[derive(Debug)]
5697struct AstMutateContext {
5698    source_range: SourceRange,
5699    node_path: Option<ast::NodePath>,
5700    command: AstMutateCommand,
5701    defined_names_stack: Vec<HashSet<String>>,
5702}
5703
5704#[derive(Debug)]
5705#[allow(clippy::large_enum_variant)]
5706enum AstMutateCommand {
5707    /// Add an expression statement to the sketch block.
5708    AddSketchBlockExprStmt {
5709        expr: ast::Expr,
5710    },
5711    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5712    AddSketchBlockVarDecl {
5713        prefix: String,
5714        expr: ast::Expr,
5715    },
5716    AddVariableDeclaration {
5717        prefix: String,
5718    },
5719    EditPoint {
5720        at: ast::Expr,
5721    },
5722    EditLine {
5723        start: ast::Expr,
5724        end: ast::Expr,
5725        construction: Option<bool>,
5726    },
5727    EditArc {
5728        start: ast::Expr,
5729        end: ast::Expr,
5730        center: ast::Expr,
5731        construction: Option<bool>,
5732    },
5733    EditCircle {
5734        start: ast::Expr,
5735        center: ast::Expr,
5736        construction: Option<bool>,
5737    },
5738    EditControlPointSpline {
5739        points: ast::Expr,
5740        construction: Option<bool>,
5741    },
5742    EditConstraintValue {
5743        value: ast::BinaryPart,
5744    },
5745    EditDistanceConstraintLabelPosition {
5746        label_position: ast::Expr,
5747    },
5748    EditCallUnlabeled {
5749        arg: ast::Expr,
5750    },
5751    EditVarInitialValue {
5752        value: Number,
5753    },
5754    DeleteNode,
5755}
5756
5757impl AstMutateCommand {
5758    fn needs_defined_names_stack(&self) -> bool {
5759        matches!(
5760            self,
5761            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5762        )
5763    }
5764}
5765
5766#[derive(Debug)]
5767enum AstMutateCommandReturn {
5768    None,
5769    Name(String),
5770}
5771
5772#[derive(Debug, Clone)]
5773struct AstNodeRef {
5774    range: SourceRange,
5775    node_path: Option<ast::NodePath>,
5776}
5777
5778impl<T> From<&ast::Node<T>> for AstNodeRef {
5779    fn from(value: &ast::Node<T>) -> Self {
5780        AstNodeRef {
5781            range: value.into(),
5782            node_path: value.node_path.clone(),
5783        }
5784    }
5785}
5786
5787impl From<&ast::BodyItem> for AstNodeRef {
5788    fn from(value: &ast::BodyItem) -> Self {
5789        match value {
5790            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5791                range: node.into(),
5792                node_path: node.node_path.clone(),
5793            },
5794            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5795                range: node.into(),
5796                node_path: node.node_path.clone(),
5797            },
5798            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5799                range: node.into(),
5800                node_path: node.node_path.clone(),
5801            },
5802            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5803                range: node.into(),
5804                node_path: node.node_path.clone(),
5805            },
5806            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5807                range: node.into(),
5808                node_path: node.node_path.clone(),
5809            },
5810        }
5811    }
5812}
5813
5814impl From<&ast::Expr> for AstNodeRef {
5815    fn from(value: &ast::Expr) -> Self {
5816        AstNodeRef {
5817            range: SourceRange::from(value),
5818            node_path: value.node_path().cloned(),
5819        }
5820    }
5821}
5822
5823impl From<&AstMutateContext> for AstNodeRef {
5824    fn from(value: &AstMutateContext) -> Self {
5825        AstNodeRef {
5826            range: value.source_range,
5827            node_path: value.node_path.clone(),
5828        }
5829    }
5830}
5831
5832impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5833    type Error = crate::walk::AstNodeError;
5834
5835    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5836        Ok(AstNodeRef {
5837            range: SourceRange::try_from(value)?,
5838            node_path: value.try_into()?,
5839        })
5840    }
5841}
5842
5843impl From<AstNodeRef> for SourceRange {
5844    fn from(value: AstNodeRef) -> Self {
5845        value.range
5846    }
5847}
5848
5849impl Visitor for AstMutateContext {
5850    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5851    type Continue = ();
5852
5853    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5854        filter_and_process(self, node)
5855    }
5856
5857    fn finish(&mut self, node: NodeMut<'_>) {
5858        match &node {
5859            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5860                self.defined_names_stack.pop();
5861            }
5862            _ => {}
5863        }
5864    }
5865}
5866
5867fn filter_and_process(
5868    ctx: &mut AstMutateContext,
5869    node: NodeMut,
5870) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5871    let Ok(node_range) = SourceRange::try_from(&node) else {
5872        // Nodes that can't be converted to a range aren't interesting.
5873        return TraversalReturn::new_continue(());
5874    };
5875    // If we're adding a variable declaration, we need to look at variable
5876    // declaration expressions to see if it already has a variable, before
5877    // continuing. The variable declaration's source range won't match the
5878    // target; its init expression will.
5879    if let NodeMut::VariableDeclaration(var_decl) = &node {
5880        let expr_range = SourceRange::from(&var_decl.declaration.init);
5881        let expr_node_path = var_decl.declaration.init.node_path();
5882        if source_ref_matches(ctx, expr_range, expr_node_path) {
5883            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5884                // We found the variable declaration expression. It doesn't need
5885                // to be added.
5886                return TraversalReturn::new_break(Ok((
5887                    AstNodeRef::from(&**var_decl),
5888                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5889                )));
5890            }
5891            if let AstMutateCommand::DeleteNode = &ctx.command {
5892                // We found the variable declaration. Delete the variable along
5893                // with the segment.
5894                return TraversalReturn {
5895                    mutate_body_item: MutateBodyItem::Delete,
5896                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5897                };
5898            }
5899        }
5900    }
5901    // Similar thing with expression statement. We need to look at the
5902    // expression inside it.
5903    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5904        let expr_range = SourceRange::from(&expr_stmt.expression);
5905        let expr_node_path = expr_stmt.expression.node_path();
5906        if source_ref_matches(ctx, expr_range, expr_node_path) {
5907            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5908                // We found the node wrapped in an expression statement. Process
5909                // the statement.
5910                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5911                    return TraversalReturn::new_continue(());
5912                };
5913                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5914            }
5915            if let AstMutateCommand::DeleteNode = &ctx.command {
5916                // We found the node wrapped in an expression statement. Delete
5917                // the whole statement.
5918                return TraversalReturn {
5919                    mutate_body_item: MutateBodyItem::Delete,
5920                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5921                };
5922            }
5923        }
5924    }
5925
5926    if ctx.command.needs_defined_names_stack() {
5927        if let NodeMut::Program(program) = &node {
5928            ctx.defined_names_stack.push(find_defined_names(*program));
5929        } else if let NodeMut::SketchBlock(block) = &node {
5930            ctx.defined_names_stack.push(find_defined_names(&block.body));
5931        }
5932    }
5933
5934    // Make sure the node matches the source ref.
5935    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5936    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5937        return TraversalReturn::new_continue(());
5938    }
5939    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5940        return TraversalReturn::new_continue(());
5941    };
5942    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5943}
5944
5945fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5946    match &ctx.node_path {
5947        Some(target) => Some(target) == node_path,
5948        None => node_range == ctx.source_range,
5949    }
5950}
5951
5952fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5953    match &ctx.command {
5954        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5955            if let NodeMut::SketchBlock(sketch_block) = node {
5956                sketch_block
5957                    .body
5958                    .items
5959                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5960                        inner: ast::ExpressionStatement {
5961                            expression: expr.clone(),
5962                            digest: None,
5963                        },
5964                        start: Default::default(),
5965                        end: Default::default(),
5966                        module_id: Default::default(),
5967                        node_path: None,
5968                        outer_attrs: Default::default(),
5969                        pre_comments: Default::default(),
5970                        comment_start: Default::default(),
5971                    }));
5972                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5973            }
5974        }
5975        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5976            if let NodeMut::SketchBlock(sketch_block) = node {
5977                let empty_defined_names = HashSet::new();
5978                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5979                let Ok(name) = next_free_name(prefix, defined_names) else {
5980                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5981                };
5982                sketch_block
5983                    .body
5984                    .items
5985                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5986                        ast::VariableDeclaration::new(
5987                            ast::VariableDeclarator::new(&name, expr.clone()),
5988                            ast::ItemVisibility::Default,
5989                            ast::VariableKind::Const,
5990                        ),
5991                    ))));
5992                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5993            }
5994        }
5995        AstMutateCommand::AddVariableDeclaration { prefix } => {
5996            if let NodeMut::VariableDeclaration(inner) = node {
5997                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5998            }
5999            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6000                let empty_defined_names = HashSet::new();
6001                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6002                let Ok(name) = next_free_name(prefix, defined_names) else {
6003                    // TODO: Return an error instead?
6004                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6005                };
6006                let mutate_node =
6007                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
6008                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6009                        ast::ItemVisibility::Default,
6010                        ast::VariableKind::Const,
6011                    ))));
6012                return TraversalReturn {
6013                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6014                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6015                };
6016            }
6017        }
6018        AstMutateCommand::EditPoint { at } => {
6019            if let NodeMut::CallExpressionKw(call) = node {
6020                if call.callee.name.name != POINT_FN {
6021                    return TraversalReturn::new_continue(());
6022                }
6023                // Update the arguments.
6024                for labeled_arg in &mut call.arguments {
6025                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6026                        labeled_arg.arg = at.clone();
6027                    }
6028                }
6029                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6030            }
6031        }
6032        AstMutateCommand::EditLine {
6033            start,
6034            end,
6035            construction,
6036        } => {
6037            if let NodeMut::CallExpressionKw(call) = node {
6038                if call.callee.name.name != LINE_FN {
6039                    return TraversalReturn::new_continue(());
6040                }
6041                // Update the arguments.
6042                for labeled_arg in &mut call.arguments {
6043                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6044                        labeled_arg.arg = start.clone();
6045                    }
6046                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6047                        labeled_arg.arg = end.clone();
6048                    }
6049                }
6050                // Handle construction kwarg
6051                if let Some(construction_value) = construction {
6052                    let construction_exists = call
6053                        .arguments
6054                        .iter()
6055                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6056                    if *construction_value {
6057                        // Add or update construction=true
6058                        if construction_exists {
6059                            // Update existing construction kwarg
6060                            for labeled_arg in &mut call.arguments {
6061                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6062                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6063                                        value: ast::LiteralValue::Bool(true),
6064                                        raw: "true".to_string(),
6065                                        digest: None,
6066                                    })));
6067                                }
6068                            }
6069                        } else {
6070                            // Add new construction kwarg
6071                            call.arguments.push(ast::LabeledArg {
6072                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6073                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6074                                    value: ast::LiteralValue::Bool(true),
6075                                    raw: "true".to_string(),
6076                                    digest: None,
6077                                }))),
6078                            });
6079                        }
6080                    } else {
6081                        // Remove construction kwarg if it exists
6082                        call.arguments
6083                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6084                    }
6085                }
6086                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6087            }
6088        }
6089        AstMutateCommand::EditArc {
6090            start,
6091            end,
6092            center,
6093            construction,
6094        } => {
6095            if let NodeMut::CallExpressionKw(call) = node {
6096                if call.callee.name.name != ARC_FN {
6097                    return TraversalReturn::new_continue(());
6098                }
6099                // Update the arguments.
6100                for labeled_arg in &mut call.arguments {
6101                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6102                        labeled_arg.arg = start.clone();
6103                    }
6104                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6105                        labeled_arg.arg = end.clone();
6106                    }
6107                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6108                        labeled_arg.arg = center.clone();
6109                    }
6110                }
6111                // Handle construction kwarg
6112                if let Some(construction_value) = construction {
6113                    let construction_exists = call
6114                        .arguments
6115                        .iter()
6116                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6117                    if *construction_value {
6118                        // Add or update construction=true
6119                        if construction_exists {
6120                            // Update existing construction kwarg
6121                            for labeled_arg in &mut call.arguments {
6122                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6123                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6124                                        value: ast::LiteralValue::Bool(true),
6125                                        raw: "true".to_string(),
6126                                        digest: None,
6127                                    })));
6128                                }
6129                            }
6130                        } else {
6131                            // Add new construction kwarg
6132                            call.arguments.push(ast::LabeledArg {
6133                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6134                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6135                                    value: ast::LiteralValue::Bool(true),
6136                                    raw: "true".to_string(),
6137                                    digest: None,
6138                                }))),
6139                            });
6140                        }
6141                    } else {
6142                        // Remove construction kwarg if it exists
6143                        call.arguments
6144                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6145                    }
6146                }
6147                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6148            }
6149        }
6150        AstMutateCommand::EditCircle {
6151            start,
6152            center,
6153            construction,
6154        } => {
6155            if let NodeMut::CallExpressionKw(call) = node {
6156                if call.callee.name.name != CIRCLE_FN {
6157                    return TraversalReturn::new_continue(());
6158                }
6159                // Update the arguments.
6160                for labeled_arg in &mut call.arguments {
6161                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6162                        labeled_arg.arg = start.clone();
6163                    }
6164                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6165                        labeled_arg.arg = center.clone();
6166                    }
6167                }
6168                // Handle construction kwarg
6169                if let Some(construction_value) = construction {
6170                    let construction_exists = call
6171                        .arguments
6172                        .iter()
6173                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6174                    if *construction_value {
6175                        if construction_exists {
6176                            // Update existing construction kwarg
6177                            for labeled_arg in &mut call.arguments {
6178                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6179                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6180                                        value: ast::LiteralValue::Bool(true),
6181                                        raw: "true".to_string(),
6182                                        digest: None,
6183                                    })));
6184                                }
6185                            }
6186                        } else {
6187                            // Add new construction kwarg
6188                            call.arguments.push(ast::LabeledArg {
6189                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6190                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6191                                    value: ast::LiteralValue::Bool(true),
6192                                    raw: "true".to_string(),
6193                                    digest: None,
6194                                }))),
6195                            });
6196                        }
6197                    } else {
6198                        // Remove construction kwarg if it exists
6199                        call.arguments
6200                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6201                    }
6202                }
6203                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6204            }
6205        }
6206        AstMutateCommand::EditControlPointSpline { points, construction } => {
6207            if let NodeMut::CallExpressionKw(call) = node {
6208                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6209                    return TraversalReturn::new_continue(());
6210                }
6211                for labeled_arg in &mut call.arguments {
6212                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6213                    {
6214                        labeled_arg.arg = points.clone();
6215                    }
6216                }
6217                // Handle construction kwarg
6218                if let Some(construction_value) = construction {
6219                    let construction_exists = call
6220                        .arguments
6221                        .iter()
6222                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6223                    if *construction_value {
6224                        if construction_exists {
6225                            for labeled_arg in &mut call.arguments {
6226                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6227                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6228                                        value: ast::LiteralValue::Bool(true),
6229                                        raw: "true".to_string(),
6230                                        digest: None,
6231                                    })));
6232                                }
6233                            }
6234                        } else {
6235                            call.arguments.push(ast::LabeledArg {
6236                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6237                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6238                                    value: ast::LiteralValue::Bool(true),
6239                                    raw: "true".to_string(),
6240                                    digest: None,
6241                                }))),
6242                            });
6243                        }
6244                    } else {
6245                        call.arguments
6246                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6247                    }
6248                }
6249                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6250            }
6251        }
6252        AstMutateCommand::EditConstraintValue { value } => {
6253            if let NodeMut::BinaryExpression(binary_expr) = node {
6254                let left_is_constraint = matches!(
6255                    &binary_expr.left,
6256                    ast::BinaryPart::CallExpressionKw(call)
6257                        if matches!(
6258                            call.callee.name.name.as_str(),
6259                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6260                        )
6261                );
6262                if left_is_constraint {
6263                    binary_expr.right = value.clone();
6264                } else {
6265                    binary_expr.left = value.clone();
6266                }
6267
6268                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6269            }
6270        }
6271        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6272            if let NodeMut::BinaryExpression(binary_expr) = node {
6273                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6274                    return TraversalReturn::new_continue(());
6275                };
6276                if !matches!(
6277                    call.callee.name.name.as_str(),
6278                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6279                ) {
6280                    return TraversalReturn::new_continue(());
6281                }
6282
6283                if let Some(label_arg) = call
6284                    .arguments
6285                    .iter_mut()
6286                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6287                {
6288                    label_arg.arg = label_position.clone();
6289                } else {
6290                    call.arguments.push(ast::LabeledArg {
6291                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6292                        arg: label_position.clone(),
6293                    });
6294                }
6295
6296                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6297            }
6298        }
6299        AstMutateCommand::EditCallUnlabeled { arg } => {
6300            if let NodeMut::CallExpressionKw(call) = node {
6301                call.unlabeled = Some(arg.clone());
6302                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6303            }
6304        }
6305        AstMutateCommand::EditVarInitialValue { value } => {
6306            // We target the SketchVar itself (matched by NodePath) rather than
6307            // the inner NumericLiteral so we can also write back into vars that
6308            // were declared without an initial value (e.g. bare `var`).
6309            if let NodeMut::SketchVar(sketch_var) = node {
6310                let Ok(literal) = to_source_number(*value) else {
6311                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6312                        "Could not convert number to AST literal: {:?}",
6313                        *value
6314                    ))));
6315                };
6316                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6317                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6318            }
6319        }
6320        AstMutateCommand::DeleteNode => {
6321            return TraversalReturn {
6322                mutate_body_item: MutateBodyItem::Delete,
6323                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6324            };
6325        }
6326    }
6327    TraversalReturn::new_continue(())
6328}
6329
6330struct FindSketchBlockSourceRange {
6331    /// The source range of the sketch block before mutation.
6332    target_before_mutation: SourceRange,
6333    /// The source range of the sketch block's last body item after mutation. We
6334    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6335    /// shared reference.
6336    found: Cell<Option<AstNodeRef>>,
6337}
6338
6339impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6340    type Error = crate::front::Error;
6341
6342    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6343        let Ok(node_range) = SourceRange::try_from(&node) else {
6344            return Ok(true);
6345        };
6346
6347        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6348            if node_range.module_id() == self.target_before_mutation.module_id()
6349                && node_range.start() == self.target_before_mutation.start()
6350                // End shouldn't match since we added something.
6351                && node_range.end() >= self.target_before_mutation.end()
6352            {
6353                self.found.set(sketch_block.body.items.last().map(|item| match item {
6354                    // For declarations like `circle1 = circle(...)`, use
6355                    // the init expression range so lookup in source_range_to_object
6356                    // matches the segment source range.
6357                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6358                    _ => AstNodeRef::from(item),
6359                }));
6360                return Ok(false);
6361            } else {
6362                // We found a different sketch block. No need to descend into
6363                // its children since sketch blocks cannot be nested.
6364                return Ok(true);
6365            }
6366        }
6367
6368        for child in node.children().iter() {
6369            if !child.visit(*self)? {
6370                return Ok(false);
6371            }
6372        }
6373
6374        Ok(true)
6375    }
6376}
6377
6378struct FindSketchBlockByNodePath {
6379    /// The Node Path of the sketch block before mutation.
6380    target_node_path: ast::NodePath,
6381    /// The ref of the sketch block's last body item after mutation. We need to
6382    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6383    /// reference.
6384    found: Cell<Option<AstNodeRef>>,
6385}
6386
6387impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6388    type Error = crate::front::Error;
6389
6390    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6391        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6392            return Ok(true);
6393        };
6394
6395        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6396            if let Some(node_path) = node_path
6397                && node_path == self.target_node_path
6398            {
6399                self.found.set(sketch_block.body.items.last().map(|item| match item {
6400                    // For declarations like `circle1 = circle(...)`, use
6401                    // the init expression range so lookup in source_range_to_object
6402                    // matches the segment source range.
6403                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6404                    _ => AstNodeRef::from(item),
6405                }));
6406
6407                return Ok(false);
6408            } else {
6409                // We found a different sketch block. No need to descend into
6410                // its children since sketch blocks cannot be nested.
6411                return Ok(true);
6412            }
6413        }
6414
6415        for child in node.children().iter() {
6416            if !child.visit(*self)? {
6417                return Ok(false);
6418            }
6419        }
6420
6421        Ok(true)
6422    }
6423}
6424
6425/// After adding an item to a sketch block, find the sketch block, and get the
6426/// source range of the added item. We assume that the added item is the last
6427/// item in the sketch block and that the sketch block's source range has grown,
6428/// but not moved from its starting offset.
6429///
6430/// TODO: Do we need to format *before* mutation in case formatting moves the
6431/// sketch block forward?
6432fn find_sketch_block_added_item(
6433    ast: &ast::Node<ast::Program>,
6434    sketch_block_before_mutation: &AstNodeRef,
6435) -> Result<AstNodeRef, KclError> {
6436    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6437        let find = FindSketchBlockByNodePath {
6438            target_node_path: node_path.clone(),
6439            found: Cell::new(None),
6440        };
6441        let node = crate::walk::Node::from(ast);
6442        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6443        find.found.into_inner().ok_or_else(|| {
6444            KclError::refactor(format!(
6445                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6446            ))
6447        })
6448    } else {
6449        // No NodePath. Fall back to legacy source range.
6450        let find = FindSketchBlockSourceRange {
6451            target_before_mutation: sketch_block_before_mutation.range,
6452            found: Cell::new(None),
6453        };
6454        let node = crate::walk::Node::from(ast);
6455        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6456        find.found.into_inner().ok_or_else(|| KclError::refactor(
6457            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?"),
6458        ))
6459    }
6460}
6461
6462fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6463    // TODO: Don't duplicate this from lib.rs Program.
6464    ast.recast_top(&Default::default(), 0)
6465}
6466
6467struct FindNumericLiteral {
6468    target: SourceRange,
6469    found: Cell<Option<ast::NumericLiteral>>,
6470}
6471
6472impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6473    type Error = crate::front::Error;
6474
6475    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6476        let Ok(node_range) = SourceRange::try_from(&node) else {
6477            return Ok(true);
6478        };
6479
6480        if node_range == self.target
6481            && let crate::walk::Node::NumericLiteral(literal) = node
6482        {
6483            self.found.set(Some(literal.inner.clone()));
6484            return Ok(false);
6485        }
6486
6487        for child in node.children().iter() {
6488            if !child.visit(*self)? {
6489                return Ok(false);
6490            }
6491        }
6492
6493        Ok(true)
6494    }
6495}
6496
6497fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6498    let find = FindNumericLiteral {
6499        target,
6500        found: Cell::new(None),
6501    };
6502    let node = crate::walk::Node::from(ast);
6503    node.visit(&find).ok()?;
6504    find.found.into_inner()
6505}
6506
6507struct FindSketchVarInitialByNodePath<'a> {
6508    target: &'a ast::NodePath,
6509    sketch_var_found: Cell<bool>,
6510    initial_literal: Cell<Option<ast::NumericLiteral>>,
6511}
6512
6513impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6514    type Error = crate::front::Error;
6515
6516    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6517        if let crate::walk::Node::SketchVar(sketch_var) = node
6518            && sketch_var.node_path.as_ref() == Some(self.target)
6519        {
6520            self.sketch_var_found.set(true);
6521            if let Some(initial) = &sketch_var.initial {
6522                self.initial_literal.set(Some(initial.inner.clone()));
6523            }
6524            return Ok(false);
6525        }
6526
6527        for child in node.children().iter() {
6528            if !child.visit(*self)? {
6529                return Ok(false);
6530            }
6531        }
6532
6533        Ok(true)
6534    }
6535}
6536
6537/// Locate the source `var` declaration corresponding to a sketch-var solution.
6538///
6539/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6540/// from "target found." The inner [`Option`] is the initial numeric literal of
6541/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6542///
6543/// When `node_path` is `None` (e.g. for older outcomes that predate the
6544/// node-path propagation), this falls back to source-range matching, which
6545/// can break under whitespace shifts elsewhere in the file.
6546fn numeric_literal_at_node_path(
6547    ast: &ast::Node<ast::Program>,
6548    node_path: Option<&ast::NodePath>,
6549    source_range: SourceRange,
6550) -> Option<Option<ast::NumericLiteral>> {
6551    let Some(node_path) = node_path else {
6552        let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6553        #[cfg(target_arch = "wasm32")]
6554        web_sys::console::warn_1(&message.into());
6555        #[cfg(not(target_arch = "wasm32"))]
6556        eprintln!("WARNING: {message}");
6557        return numeric_literal_at_source_range(ast, source_range).map(Some);
6558    };
6559    let find = FindSketchVarInitialByNodePath {
6560        target: node_path,
6561        sketch_var_found: Cell::new(false),
6562        initial_literal: Cell::new(None),
6563    };
6564    let node = crate::walk::Node::from(ast);
6565    node.visit(&find).ok()?;
6566    if !find.sketch_var_found.get() {
6567        return None;
6568    }
6569    Some(find.initial_literal.into_inner())
6570}
6571
6572fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6573    match suffix {
6574        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6575        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6576        NumericSuffix::M => Some(UnitLength::Meters),
6577        NumericSuffix::Inch => Some(UnitLength::Inches),
6578        NumericSuffix::Ft => Some(UnitLength::Feet),
6579        NumericSuffix::Yd => Some(UnitLength::Yards),
6580        _ => None,
6581    }
6582}
6583
6584fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6585    match suffix_length_unit(number.units) {
6586        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6587        None => number.value,
6588    }
6589}
6590
6591fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6592    match suffix_length_unit(literal.suffix) {
6593        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6594        None => literal.value,
6595    }
6596}
6597
6598fn var_solution_needs_commit(
6599    current_literal: &ast::NumericLiteral,
6600    solved_value: Number,
6601    default_length_unit: UnitLength,
6602) -> bool {
6603    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6604    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6605
6606    (current - solved).abs() > 1e-9
6607}
6608
6609fn preserve_var_solution_literal_style(
6610    current_literal: &ast::NumericLiteral,
6611    solved_value: Number,
6612    default_length_unit: UnitLength,
6613) -> Number {
6614    if current_literal.suffix == NumericSuffix::None {
6615        return Number {
6616            value: number_value_in_default_length_units(solved_value, default_length_unit),
6617            units: NumericSuffix::None,
6618        };
6619    }
6620
6621    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6622        return solved_value;
6623    };
6624
6625    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6626    Number {
6627        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6628        units: current_literal.suffix,
6629    }
6630}
6631
6632pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6633    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6634        inner: ast::ArrayExpression {
6635            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6636            non_code_meta: Default::default(),
6637            digest: None,
6638        },
6639        start: Default::default(),
6640        end: Default::default(),
6641        module_id: Default::default(),
6642        node_path: None,
6643        outer_attrs: Default::default(),
6644        pre_comments: Default::default(),
6645        comment_start: Default::default(),
6646    })))
6647}
6648
6649pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6650    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6651        ast::ArrayExpression {
6652            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6653            digest: None,
6654            non_code_meta: Default::default(),
6655        },
6656    ))))
6657}
6658
6659fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6660    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6661        ast::ArrayExpression {
6662            elements: vec![
6663                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6664                    point.x,
6665                )?)))),
6666                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6667                    point.y,
6668                )?)))),
6669            ],
6670            non_code_meta: Default::default(),
6671            digest: None,
6672        },
6673    ))))
6674}
6675
6676fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6677    match expr {
6678        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6679            inner: ast::Literal::from(to_source_number(*number)?),
6680            start: Default::default(),
6681            end: Default::default(),
6682            module_id: Default::default(),
6683            node_path: None,
6684            outer_attrs: Default::default(),
6685            pre_comments: Default::default(),
6686            comment_start: Default::default(),
6687        }))),
6688        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6689            inner: ast::SketchVar {
6690                initial: Some(Box::new(ast::Node {
6691                    inner: to_source_number(*number)?,
6692                    start: Default::default(),
6693                    end: Default::default(),
6694                    module_id: Default::default(),
6695                    node_path: None,
6696                    outer_attrs: Default::default(),
6697                    pre_comments: Default::default(),
6698                    comment_start: Default::default(),
6699                })),
6700                digest: None,
6701            },
6702            start: Default::default(),
6703            end: Default::default(),
6704            module_id: Default::default(),
6705            node_path: None,
6706            outer_attrs: Default::default(),
6707            pre_comments: Default::default(),
6708            comment_start: Default::default(),
6709        }))),
6710        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6711    }
6712}
6713
6714fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6715    Ok(ast::NumericLiteral {
6716        value: number.value,
6717        suffix: number.units,
6718        raw: format_number_literal(number.value, number.units, None)?,
6719        digest: None,
6720    })
6721}
6722
6723pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6724    ast::Expr::Name(Box::new(ast_name(name)))
6725}
6726
6727fn ast_name(name: String) -> ast::Node<ast::Name> {
6728    ast::Node {
6729        inner: ast::Name {
6730            name: ast::Node {
6731                inner: ast::Identifier { name, digest: None },
6732                start: Default::default(),
6733                end: Default::default(),
6734                module_id: Default::default(),
6735                node_path: None,
6736                outer_attrs: Default::default(),
6737                pre_comments: Default::default(),
6738                comment_start: Default::default(),
6739            },
6740            path: Vec::new(),
6741            abs_path: false,
6742            digest: None,
6743        },
6744        start: Default::default(),
6745        end: Default::default(),
6746        module_id: Default::default(),
6747        node_path: None,
6748        outer_attrs: Default::default(),
6749        pre_comments: Default::default(),
6750        comment_start: Default::default(),
6751    }
6752}
6753
6754pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6755    ast::Name {
6756        name: ast::Node {
6757            inner: ast::Identifier {
6758                name: name.to_owned(),
6759                digest: None,
6760            },
6761            start: Default::default(),
6762            end: Default::default(),
6763            module_id: Default::default(),
6764            node_path: None,
6765            outer_attrs: Default::default(),
6766            pre_comments: Default::default(),
6767            comment_start: Default::default(),
6768        },
6769        path: Default::default(),
6770        abs_path: false,
6771        digest: None,
6772    }
6773}
6774
6775// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6776
6777/// Create an AST node for coincident([expr1, expr2, ...])
6778pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6779    let elements = exprs.into_iter().collect::<Vec<_>>();
6780    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6781
6782    // Create array [expr1, expr2, ...]
6783    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6784        elements,
6785        digest: None,
6786        non_code_meta: Default::default(),
6787    })));
6788
6789    // Create coincident([...])
6790    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6791        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6792        unlabeled: Some(array_expr),
6793        arguments: Default::default(),
6794        digest: None,
6795        non_code_meta: Default::default(),
6796    })))
6797}
6798
6799/// Create an AST node for line(start = [...], end = [...])
6800pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6801    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6802        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6803        unlabeled: None,
6804        arguments: vec![
6805            ast::LabeledArg {
6806                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6807                arg: start_ast,
6808            },
6809            ast::LabeledArg {
6810                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6811                arg: end_ast,
6812            },
6813        ],
6814        digest: None,
6815        non_code_meta: Default::default(),
6816    })))
6817}
6818
6819/// Create an AST node for arc(start = [...], end = [...], center = [...])
6820pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6821    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6822        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6823        unlabeled: None,
6824        arguments: vec![
6825            ast::LabeledArg {
6826                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6827                arg: start_ast,
6828            },
6829            ast::LabeledArg {
6830                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6831                arg: end_ast,
6832            },
6833            ast::LabeledArg {
6834                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6835                arg: center_ast,
6836            },
6837        ],
6838        digest: None,
6839        non_code_meta: Default::default(),
6840    })))
6841}
6842
6843/// Create an AST node for circle(start = [...], center = [...])
6844pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6845    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6846        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6847        unlabeled: None,
6848        arguments: vec![
6849            ast::LabeledArg {
6850                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6851                arg: start_ast,
6852            },
6853            ast::LabeledArg {
6854                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6855                arg: center_ast,
6856            },
6857        ],
6858        digest: None,
6859        non_code_meta: Default::default(),
6860    })))
6861}
6862
6863/// Create an AST node for horizontal(line)
6864pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6865    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6866        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6867        unlabeled: Some(line_expr),
6868        arguments: Default::default(),
6869        digest: None,
6870        non_code_meta: Default::default(),
6871    })))
6872}
6873
6874/// Create an AST node for vertical(line)
6875pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6876    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6877        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6878        unlabeled: Some(line_expr),
6879        arguments: Default::default(),
6880        digest: None,
6881        non_code_meta: Default::default(),
6882    })))
6883}
6884
6885/// Create a member expression like object.property (e.g., line1.end)
6886pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6887    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6888        object: object_expr,
6889        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6890            name: ast::Node::no_src(ast::Identifier {
6891                name: property.to_string(),
6892                digest: None,
6893            }),
6894            path: Vec::new(),
6895            abs_path: false,
6896            digest: None,
6897        }))),
6898        computed: false,
6899        digest: None,
6900    })))
6901}
6902
6903pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6904    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6905        object: object_expr,
6906        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6907            value: index as f64,
6908            suffix: NumericSuffix::None,
6909            raw: index.to_string(),
6910            digest: None,
6911        })))),
6912        computed: true,
6913        digest: None,
6914    })))
6915}
6916
6917/// Create an AST node for `fixed([point, [x, y]])`.
6918fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6919    // Create [x, y] array literal.
6920    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6921        position.x,
6922    )?))));
6923    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6924        position.y,
6925    )?))));
6926    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6927        elements: vec![x_literal, y_literal],
6928        digest: None,
6929        non_code_meta: Default::default(),
6930    })));
6931
6932    // Create [point, [x, y]] outer array.
6933    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6934        elements: vec![point_expr, point_array],
6935        digest: None,
6936        non_code_meta: Default::default(),
6937    })));
6938
6939    // Create fixed([...])
6940    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6941        ast::CallExpressionKw {
6942            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6943            unlabeled: Some(array_expr),
6944            arguments: Default::default(),
6945            digest: None,
6946            non_code_meta: Default::default(),
6947        },
6948    ))))
6949}
6950
6951/// Create an AST node for equalLength([line1, line2, ...])
6952pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6953    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6954        elements: line_exprs,
6955        digest: None,
6956        non_code_meta: Default::default(),
6957    })));
6958
6959    // Create equalLength([...])
6960    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6961        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6962        unlabeled: Some(array_expr),
6963        arguments: Default::default(),
6964        digest: None,
6965        non_code_meta: Default::default(),
6966    })))
6967}
6968
6969/// Create an AST node for equalRadius([seg1, seg2, ...])
6970pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6971    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6972        elements: segment_exprs,
6973        digest: None,
6974        non_code_meta: Default::default(),
6975    })));
6976
6977    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6978        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6979        unlabeled: Some(array_expr),
6980        arguments: Default::default(),
6981        digest: None,
6982        non_code_meta: Default::default(),
6983    })))
6984}
6985
6986/// Create an AST node for tangent([seg1, seg2])
6987pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6988    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6989        elements: vec![seg1_expr, seg2_expr],
6990        digest: None,
6991        non_code_meta: Default::default(),
6992    })));
6993
6994    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6995        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6996        unlabeled: Some(array_expr),
6997        arguments: Default::default(),
6998        digest: None,
6999        non_code_meta: Default::default(),
7000    })))
7001}
7002
7003/// Create an AST node for symmetric([input1, input2], axis = line)
7004pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7005    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7006        elements: input_exprs,
7007        digest: None,
7008        non_code_meta: Default::default(),
7009    })));
7010    let arguments = vec![ast::LabeledArg {
7011        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7012        arg: axis_expr,
7013    }];
7014
7015    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7016        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7017        unlabeled: Some(array_expr),
7018        arguments,
7019        digest: None,
7020        non_code_meta: Default::default(),
7021    })))
7022}
7023
7024/// Create an AST node for midpoint(segment, point = point)
7025pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7026    let arguments = vec![ast::LabeledArg {
7027        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7028        arg: point_expr,
7029    }];
7030
7031    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7032        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7033        unlabeled: Some(segment_expr),
7034        arguments,
7035        digest: None,
7036        non_code_meta: Default::default(),
7037    })))
7038}
7039
7040#[cfg(test)]
7041mod tests {
7042    use std::sync;
7043
7044    use super::*;
7045    use crate::engine::PlaneName;
7046    use crate::engine::engine_manager::EngineManager;
7047    use crate::execution::cache::SketchModeState;
7048    use crate::execution::cache::clear_mem_cache;
7049    use crate::execution::cache::read_old_memory;
7050    use crate::execution::cache::write_old_memory;
7051    use crate::front::Distance;
7052    use crate::front::Fixed;
7053    use crate::front::FixedPoint;
7054    use crate::front::Midpoint;
7055    use crate::front::Object;
7056    use crate::front::Plane;
7057    use crate::front::Sketch;
7058    use crate::front::Tangent;
7059    use crate::frontend::sketch::Vertical;
7060    use crate::pretty::NumericSuffix;
7061
7062    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7063        for object in &scene_graph.objects {
7064            if let ObjectKind::Sketch(_) = &object.kind {
7065                return Some(object);
7066            }
7067        }
7068        None
7069    }
7070
7071    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7072        for object in &scene_graph.objects {
7073            if let ObjectKind::Face(_) = &object.kind {
7074                return Some(object);
7075            }
7076        }
7077        None
7078    }
7079
7080    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7081        for object in &scene_graph.objects {
7082            if matches!(&object.kind, ObjectKind::Wall(_)) {
7083                return Some(object.id);
7084            }
7085        }
7086        None
7087    }
7088
7089    fn find_cap_object_id_with_solid_output_index(
7090        scene_graph: &SceneGraph,
7091        cap_kind: crate::frontend::api::CapKind,
7092        solid_output_index: usize,
7093    ) -> Option<ObjectId> {
7094        for object in &scene_graph.objects {
7095            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7096            {
7097                return Some(object.id);
7098            }
7099        }
7100        None
7101    }
7102
7103    #[test]
7104    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7105        let source = "\
7106region001 = region(point = [0.1, 0.1], sketch = s)
7107extrude001 = extrude(region001, length = 5)
7108revolve001 = revolve(region001, axis = Y)
7109sweep001 = sweep(region001, path = path001)
7110loft001 = loft(region001)
7111not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7112";
7113
7114        let program = Program::parse(source).unwrap().0.unwrap();
7115
7116        assert_eq!(
7117            region_name_from_sweep_variable(&program.ast, "extrude001"),
7118            Some("region001".to_owned())
7119        );
7120        assert_eq!(
7121            region_name_from_sweep_variable(&program.ast, "revolve001"),
7122            Some("region001".to_owned())
7123        );
7124        assert_eq!(
7125            region_name_from_sweep_variable(&program.ast, "sweep001"),
7126            Some("region001".to_owned())
7127        );
7128        assert_eq!(
7129            region_name_from_sweep_variable(&program.ast, "loft001"),
7130            Some("region001".to_owned())
7131        );
7132        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7133    }
7134
7135    #[track_caller]
7136    fn expect_sketch(object: &Object) -> &Sketch {
7137        if let ObjectKind::Sketch(sketch) = &object.kind {
7138            sketch
7139        } else {
7140            panic!("Object is not a sketch: {:?}", object);
7141        }
7142    }
7143
7144    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7145        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7146        let ObjectKind::Segment {
7147            segment: Segment::Point(point),
7148        } = &point_object.kind
7149        else {
7150            panic!("Object is not a point segment: {point_object:?}");
7151        };
7152        point.position.clone()
7153    }
7154
7155    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7156        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7157        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7158    }
7159
7160    /// Build a millimeter-valued point expression for concise sketch edit test
7161    /// setup.
7162    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7163        Point2d {
7164            x: Expr::Var(Number {
7165                value: x,
7166                units: NumericSuffix::Mm,
7167            }),
7168            y: Expr::Var(Number {
7169                value: y,
7170                units: NumericSuffix::Mm,
7171            }),
7172        }
7173    }
7174
7175    /// Build a millimeter-valued numeric point for comparing solved scene graph
7176    /// positions.
7177    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7178        Point2d {
7179            x: Number {
7180                value: x,
7181                units: NumericSuffix::Mm,
7182            },
7183            y: Number {
7184                value: y,
7185                units: NumericSuffix::Mm,
7186            },
7187        }
7188    }
7189
7190    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7191        LineCtor {
7192            start: Point2d {
7193                x: Expr::Number(Number { value: start_x, units }),
7194                y: Expr::Number(Number { value: start_y, units }),
7195            },
7196            end: Point2d {
7197                x: Expr::Number(Number { value: end_x, units }),
7198                y: Expr::Number(Number { value: end_y, units }),
7199            },
7200            construction: None,
7201        }
7202    }
7203
7204    async fn create_sketch_with_single_line(
7205        frontend: &mut FrontendState,
7206        ctx: &ExecutorContext,
7207        mock_ctx: &ExecutorContext,
7208        version: Version,
7209    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7210        frontend.program = Program::empty();
7211
7212        let sketch_args = SketchCtor {
7213            on: Plane::Default(PlaneName::Xy),
7214        };
7215        let (_src_delta, _scene_delta, sketch_id) = frontend
7216            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7217            .await
7218            .unwrap();
7219
7220        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7221        let (source_delta, scene_graph_delta) = frontend
7222            .add_segment(mock_ctx, version, sketch_id, segment, None)
7223            .await
7224            .unwrap();
7225        let line_id = *scene_graph_delta
7226            .new_objects
7227            .last()
7228            .expect("Expected line object id to be created");
7229
7230        (sketch_id, line_id, source_delta, scene_graph_delta)
7231    }
7232
7233    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7234        frontend.program = program.clone();
7235        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7236        frontend.update_state_after_exec(outcome, true);
7237    }
7238
7239    #[tokio::test(flavor = "multi_thread")]
7240    async fn test_sketch_checkpoint_round_trip_restores_state() {
7241        let mut frontend = FrontendState::new();
7242        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7243        let mock_ctx = ExecutorContext::new_mock(None).await;
7244        let version = Version(0);
7245
7246        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7247            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7248
7249        let expected_source = source_delta.text.clone();
7250        let expected_scene_graph = frontend.scene_graph.clone();
7251        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7252        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7253
7254        let checkpoint_id = frontend
7255            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7256            .await
7257            .unwrap();
7258
7259        let edited_segments = vec![ExistingSegmentCtor {
7260            id: line_id,
7261            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7262        }];
7263        let (edited_source, _edited_scene) = frontend
7264            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7265            .await
7266            .unwrap();
7267        assert_ne!(edited_source.text, expected_source);
7268
7269        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7270
7271        assert_eq!(restored.source_delta.text, expected_source);
7272        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7273        assert!(restored.scene_graph_delta.invalidates_ids);
7274        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7275        assert_eq!(frontend.scene_graph, expected_scene_graph);
7276        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7277
7278        ctx.close().await;
7279    }
7280
7281    #[tokio::test(flavor = "multi_thread")]
7282    async fn test_sketch_checkpoints_prune_oldest_entries() {
7283        let mut frontend = FrontendState::new();
7284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7285        let mock_ctx = ExecutorContext::new_mock(None).await;
7286        let version = Version(0);
7287
7288        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7289            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7290
7291        let mut checkpoint_ids = Vec::new();
7292        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7293            checkpoint_ids.push(
7294                frontend
7295                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7296                    .await
7297                    .unwrap(),
7298            );
7299        }
7300
7301        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7302        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7303
7304        let oldest_retained = checkpoint_ids[3];
7305        assert_eq!(
7306            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7307            Some(oldest_retained)
7308        );
7309
7310        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7311        assert!(evicted_restore.is_err());
7312        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7313
7314        frontend
7315            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7316            .await
7317            .unwrap();
7318
7319        ctx.close().await;
7320    }
7321
7322    #[tokio::test(flavor = "multi_thread")]
7323    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7324        let mut frontend = FrontendState::new();
7325        let missing_checkpoint = SketchCheckpointId::new(999);
7326
7327        let err = frontend
7328            .restore_sketch_checkpoint(missing_checkpoint)
7329            .await
7330            .expect_err("Expected restore to fail for missing checkpoint");
7331
7332        assert!(err.msg.contains("Sketch checkpoint not found"));
7333    }
7334
7335    #[tokio::test(flavor = "multi_thread")]
7336    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7337        let mut frontend = FrontendState::new();
7338        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7339        let mock_ctx = ExecutorContext::new_mock(None).await;
7340        let version = Version(0);
7341
7342        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7343            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7344
7345        let checkpoint_a = frontend
7346            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7347            .await
7348            .unwrap();
7349        let checkpoint_b = frontend
7350            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7351            .await
7352            .unwrap();
7353        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7354
7355        frontend.clear_sketch_checkpoints();
7356        assert!(frontend.sketch_checkpoints.is_empty());
7357        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7358        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7359
7360        ctx.close().await;
7361    }
7362
7363    #[tokio::test(flavor = "multi_thread")]
7364    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7365        let mut frontend = FrontendState::new();
7366        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7367        let mock_ctx = ExecutorContext::new_mock(None).await;
7368        let version = Version(0);
7369
7370        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7371            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7372        let old_source = source_delta.text.clone();
7373        let old_checkpoint = frontend
7374            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7375            .await
7376            .unwrap();
7377        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7378
7379        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7380            .unwrap()
7381            .0
7382            .unwrap();
7383
7384        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7385        let SetProgramOutcome::Success {
7386            checkpoint_id: Some(new_checkpoint),
7387            ..
7388        } = result
7389        else {
7390            panic!("Expected Success with a fresh checkpoint baseline");
7391        };
7392
7393        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7394
7395        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7396        assert_eq!(old_restore.source_delta.text, old_source);
7397
7398        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7399        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7400
7401        ctx.close().await;
7402    }
7403
7404    #[tokio::test(flavor = "multi_thread")]
7405    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7406        let mut frontend = FrontendState::new();
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        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7412            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7413        let old_checkpoint = frontend
7414            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7415            .await
7416            .unwrap();
7417        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7418
7419        let failing_program = Program::parse(
7420            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7421        )
7422        .unwrap()
7423        .0
7424        .unwrap();
7425
7426        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7427        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7428        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7429        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7430
7431        ctx.close().await;
7432    }
7433
7434    #[tokio::test(flavor = "multi_thread")]
7435    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7436        let mut frontend = FrontendState::new();
7437        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7438
7439        let program = Program::parse(
7440            "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",
7441        )
7442        .unwrap()
7443        .0
7444        .unwrap();
7445        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7446        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7447            panic!("Expected successful baseline program execution");
7448        };
7449
7450        clear_mem_cache().await;
7451        assert!(read_old_memory().await.is_none());
7452
7453        let checkpoint_without_mock_memory = frontend
7454            .create_sketch_checkpoint((*exec_outcome).clone())
7455            .await
7456            .unwrap();
7457
7458        write_old_memory(SketchModeState::new_for_tests()).await;
7459        assert!(read_old_memory().await.is_some());
7460
7461        let checkpoint_with_mock_memory = frontend
7462            .create_sketch_checkpoint((*exec_outcome).clone())
7463            .await
7464            .unwrap();
7465
7466        clear_mem_cache().await;
7467        assert!(read_old_memory().await.is_none());
7468
7469        frontend
7470            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7471            .await
7472            .unwrap();
7473        assert!(read_old_memory().await.is_some());
7474
7475        frontend
7476            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7477            .await
7478            .unwrap();
7479        assert!(read_old_memory().await.is_none());
7480
7481        ctx.close().await;
7482    }
7483
7484    #[tokio::test(flavor = "multi_thread")]
7485    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7486        let source = "\
7487sketch(on = XY) {
7488  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7489}
7490
7491bad = missing_name
7492";
7493        let program = Program::parse(source).unwrap().0.unwrap();
7494
7495        let mut frontend = FrontendState::new();
7496
7497        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7498        let mock_ctx = ExecutorContext::new_mock(None).await;
7499        let version = Version(0);
7500        let project_id = ProjectId(0);
7501        let file_id = FileId(0);
7502
7503        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7504            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7505        };
7506
7507        let sketch_id = frontend
7508            .scene_graph
7509            .objects
7510            .iter()
7511            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7512            .expect("Expected sketch object from errored hack_set_program");
7513
7514        frontend
7515            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7516            .await
7517            .unwrap();
7518
7519        ctx.close().await;
7520        mock_ctx.close().await;
7521    }
7522
7523    #[tokio::test(flavor = "multi_thread")]
7524    async fn test_new_sketch_add_point_edit_point() {
7525        let program = Program::empty();
7526
7527        let mut frontend = FrontendState::new();
7528        frontend.program = program;
7529
7530        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7531        let mock_ctx = ExecutorContext::new_mock(None).await;
7532        let version = Version(0);
7533
7534        let sketch_args = SketchCtor {
7535            on: Plane::Default(PlaneName::Xy),
7536        };
7537        let (_src_delta, scene_delta, sketch_id) = frontend
7538            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7539            .await
7540            .unwrap();
7541        assert_eq!(sketch_id, ObjectId(1));
7542        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7543        let sketch_object = &scene_delta.new_graph.objects[1];
7544        assert_eq!(sketch_object.id, ObjectId(1));
7545        assert_eq!(
7546            sketch_object.kind,
7547            ObjectKind::Sketch(Sketch {
7548                args: SketchCtor {
7549                    on: Plane::Default(PlaneName::Xy)
7550                },
7551                plane: ObjectId(0),
7552                segments: vec![],
7553                constraints: vec![],
7554            })
7555        );
7556        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7557
7558        let point_ctor = PointCtor {
7559            position: Point2d {
7560                x: Expr::Number(Number {
7561                    value: 1.0,
7562                    units: NumericSuffix::Inch,
7563                }),
7564                y: Expr::Number(Number {
7565                    value: 2.0,
7566                    units: NumericSuffix::Inch,
7567                }),
7568            },
7569        };
7570        let segment = SegmentCtor::Point(point_ctor);
7571        let (src_delta, scene_delta) = frontend
7572            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7573            .await
7574            .unwrap();
7575        assert_eq!(
7576            src_delta.text.as_str(),
7577            "sketch001 = sketch(on = XY) {
7578  point(at = [1in, 2in])
7579}
7580"
7581        );
7582        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7583        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7584        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7585            assert_eq!(scene_object.id.0, i);
7586        }
7587
7588        let point_id = *scene_delta.new_objects.last().unwrap();
7589
7590        let point_ctor = PointCtor {
7591            position: Point2d {
7592                x: Expr::Number(Number {
7593                    value: 3.0,
7594                    units: NumericSuffix::Inch,
7595                }),
7596                y: Expr::Number(Number {
7597                    value: 4.0,
7598                    units: NumericSuffix::Inch,
7599                }),
7600            },
7601        };
7602        let segments = vec![ExistingSegmentCtor {
7603            id: point_id,
7604            ctor: SegmentCtor::Point(point_ctor),
7605        }];
7606        let (src_delta, scene_delta) = frontend
7607            .edit_segments(&mock_ctx, version, sketch_id, segments)
7608            .await
7609            .unwrap();
7610        assert_eq!(
7611            src_delta.text.as_str(),
7612            "sketch001 = sketch(on = XY) {
7613  point(at = [3in, 4in])
7614}
7615"
7616        );
7617        assert_eq!(scene_delta.new_objects, vec![]);
7618        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7619
7620        ctx.close().await;
7621        mock_ctx.close().await;
7622    }
7623
7624    #[tokio::test(flavor = "multi_thread")]
7625    async fn test_new_sketch_add_line_edit_line() {
7626        let program = Program::empty();
7627
7628        let mut frontend = FrontendState::new();
7629        frontend.program = program;
7630
7631        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7632        let mock_ctx = ExecutorContext::new_mock(None).await;
7633        let version = Version(0);
7634
7635        let sketch_args = SketchCtor {
7636            on: Plane::Default(PlaneName::Xy),
7637        };
7638        let (_src_delta, scene_delta, sketch_id) = frontend
7639            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7640            .await
7641            .unwrap();
7642        assert_eq!(sketch_id, ObjectId(1));
7643        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7644        let sketch_object = &scene_delta.new_graph.objects[1];
7645        assert_eq!(sketch_object.id, ObjectId(1));
7646        assert_eq!(
7647            sketch_object.kind,
7648            ObjectKind::Sketch(Sketch {
7649                args: SketchCtor {
7650                    on: Plane::Default(PlaneName::Xy)
7651                },
7652                plane: ObjectId(0),
7653                segments: vec![],
7654                constraints: vec![],
7655            })
7656        );
7657        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7658
7659        let line_ctor = LineCtor {
7660            start: Point2d {
7661                x: Expr::Number(Number {
7662                    value: 0.0,
7663                    units: NumericSuffix::Mm,
7664                }),
7665                y: Expr::Number(Number {
7666                    value: 0.0,
7667                    units: NumericSuffix::Mm,
7668                }),
7669            },
7670            end: Point2d {
7671                x: Expr::Number(Number {
7672                    value: 10.0,
7673                    units: NumericSuffix::Mm,
7674                }),
7675                y: Expr::Number(Number {
7676                    value: 10.0,
7677                    units: NumericSuffix::Mm,
7678                }),
7679            },
7680            construction: None,
7681        };
7682        let segment = SegmentCtor::Line(line_ctor);
7683        let (src_delta, scene_delta) = frontend
7684            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7685            .await
7686            .unwrap();
7687        assert_eq!(
7688            src_delta.text.as_str(),
7689            "sketch001 = sketch(on = XY) {
7690  line(start = [0mm, 0mm], end = [10mm, 10mm])
7691}
7692"
7693        );
7694        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7695        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7696        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7697            assert_eq!(scene_object.id.0, i);
7698        }
7699
7700        // The new objects are the end points and then the line.
7701        let line = *scene_delta.new_objects.last().unwrap();
7702
7703        let line_ctor = LineCtor {
7704            start: Point2d {
7705                x: Expr::Number(Number {
7706                    value: 1.0,
7707                    units: NumericSuffix::Mm,
7708                }),
7709                y: Expr::Number(Number {
7710                    value: 2.0,
7711                    units: NumericSuffix::Mm,
7712                }),
7713            },
7714            end: Point2d {
7715                x: Expr::Number(Number {
7716                    value: 13.0,
7717                    units: NumericSuffix::Mm,
7718                }),
7719                y: Expr::Number(Number {
7720                    value: 14.0,
7721                    units: NumericSuffix::Mm,
7722                }),
7723            },
7724            construction: None,
7725        };
7726        let segments = vec![ExistingSegmentCtor {
7727            id: line,
7728            ctor: SegmentCtor::Line(line_ctor),
7729        }];
7730        let (src_delta, scene_delta) = frontend
7731            .edit_segments(&mock_ctx, version, sketch_id, segments)
7732            .await
7733            .unwrap();
7734        assert_eq!(
7735            src_delta.text.as_str(),
7736            "sketch001 = sketch(on = XY) {
7737  line(start = [1mm, 2mm], end = [13mm, 14mm])
7738}
7739"
7740        );
7741        assert_eq!(scene_delta.new_objects, vec![]);
7742        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7743
7744        ctx.close().await;
7745        mock_ctx.close().await;
7746    }
7747
7748    #[tokio::test(flavor = "multi_thread")]
7749    async fn test_new_sketch_add_arc_edit_arc() {
7750        let program = Program::empty();
7751
7752        let mut frontend = FrontendState::new();
7753        frontend.program = program;
7754
7755        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7756        let mock_ctx = ExecutorContext::new_mock(None).await;
7757        let version = Version(0);
7758
7759        let sketch_args = SketchCtor {
7760            on: Plane::Default(PlaneName::Xy),
7761        };
7762        let (_src_delta, scene_delta, sketch_id) = frontend
7763            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7764            .await
7765            .unwrap();
7766        assert_eq!(sketch_id, ObjectId(1));
7767        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7768        let sketch_object = &scene_delta.new_graph.objects[1];
7769        assert_eq!(sketch_object.id, ObjectId(1));
7770        assert_eq!(
7771            sketch_object.kind,
7772            ObjectKind::Sketch(Sketch {
7773                args: SketchCtor {
7774                    on: Plane::Default(PlaneName::Xy),
7775                },
7776                plane: ObjectId(0),
7777                segments: vec![],
7778                constraints: vec![],
7779            })
7780        );
7781        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7782
7783        let arc_ctor = ArcCtor {
7784            start: Point2d {
7785                x: Expr::Var(Number {
7786                    value: 0.0,
7787                    units: NumericSuffix::Mm,
7788                }),
7789                y: Expr::Var(Number {
7790                    value: 0.0,
7791                    units: NumericSuffix::Mm,
7792                }),
7793            },
7794            end: Point2d {
7795                x: Expr::Var(Number {
7796                    value: 10.0,
7797                    units: NumericSuffix::Mm,
7798                }),
7799                y: Expr::Var(Number {
7800                    value: 10.0,
7801                    units: NumericSuffix::Mm,
7802                }),
7803            },
7804            center: Point2d {
7805                x: Expr::Var(Number {
7806                    value: 10.0,
7807                    units: NumericSuffix::Mm,
7808                }),
7809                y: Expr::Var(Number {
7810                    value: 0.0,
7811                    units: NumericSuffix::Mm,
7812                }),
7813            },
7814            construction: None,
7815        };
7816        let segment = SegmentCtor::Arc(arc_ctor);
7817        let (src_delta, scene_delta) = frontend
7818            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7819            .await
7820            .unwrap();
7821        assert_eq!(
7822            src_delta.text.as_str(),
7823            "sketch001 = sketch(on = XY) {
7824  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7825}
7826"
7827        );
7828        assert_eq!(
7829            scene_delta.new_objects,
7830            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7831        );
7832        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7833            assert_eq!(scene_object.id.0, i);
7834        }
7835        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7836
7837        // The new objects are the end points, the center, and then the arc.
7838        let arc = *scene_delta.new_objects.last().unwrap();
7839
7840        let arc_ctor = ArcCtor {
7841            start: Point2d {
7842                x: Expr::Var(Number {
7843                    value: 1.0,
7844                    units: NumericSuffix::Mm,
7845                }),
7846                y: Expr::Var(Number {
7847                    value: 2.0,
7848                    units: NumericSuffix::Mm,
7849                }),
7850            },
7851            end: Point2d {
7852                x: Expr::Var(Number {
7853                    value: 13.0,
7854                    units: NumericSuffix::Mm,
7855                }),
7856                y: Expr::Var(Number {
7857                    value: 14.0,
7858                    units: NumericSuffix::Mm,
7859                }),
7860            },
7861            center: Point2d {
7862                x: Expr::Var(Number {
7863                    value: 13.0,
7864                    units: NumericSuffix::Mm,
7865                }),
7866                y: Expr::Var(Number {
7867                    value: 2.0,
7868                    units: NumericSuffix::Mm,
7869                }),
7870            },
7871            construction: None,
7872        };
7873        let segments = vec![ExistingSegmentCtor {
7874            id: arc,
7875            ctor: SegmentCtor::Arc(arc_ctor),
7876        }];
7877        let (src_delta, scene_delta) = frontend
7878            .edit_segments(&mock_ctx, version, sketch_id, segments)
7879            .await
7880            .unwrap();
7881        assert_eq!(
7882            src_delta.text.as_str(),
7883            "sketch001 = sketch(on = XY) {
7884  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7885}
7886"
7887        );
7888        assert_eq!(scene_delta.new_objects, vec![]);
7889        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7890
7891        ctx.close().await;
7892        mock_ctx.close().await;
7893    }
7894
7895    #[tokio::test(flavor = "multi_thread")]
7896    async fn test_new_sketch_add_circle_edit_circle() {
7897        let program = Program::empty();
7898
7899        let mut frontend = FrontendState::new();
7900        frontend.program = program;
7901
7902        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7903        let mock_ctx = ExecutorContext::new_mock(None).await;
7904        let version = Version(0);
7905
7906        let sketch_args = SketchCtor {
7907            on: Plane::Default(PlaneName::Xy),
7908        };
7909        let (_src_delta, _scene_delta, sketch_id) = frontend
7910            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7911            .await
7912            .unwrap();
7913
7914        // Add a circle segment.
7915        let circle_ctor = CircleCtor {
7916            start: Point2d {
7917                x: Expr::Var(Number {
7918                    value: 5.0,
7919                    units: NumericSuffix::Mm,
7920                }),
7921                y: Expr::Var(Number {
7922                    value: 0.0,
7923                    units: NumericSuffix::Mm,
7924                }),
7925            },
7926            center: Point2d {
7927                x: Expr::Var(Number {
7928                    value: 0.0,
7929                    units: NumericSuffix::Mm,
7930                }),
7931                y: Expr::Var(Number {
7932                    value: 0.0,
7933                    units: NumericSuffix::Mm,
7934                }),
7935            },
7936            construction: None,
7937        };
7938        let segment = SegmentCtor::Circle(circle_ctor);
7939        let (src_delta, scene_delta) = frontend
7940            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7941            .await
7942            .unwrap();
7943        assert_eq!(
7944            src_delta.text.as_str(),
7945            "sketch001 = sketch(on = XY) {
7946  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7947}
7948"
7949        );
7950        // The new objects are start, center, and then the circle segment.
7951        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7952        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7953
7954        let circle = *scene_delta.new_objects.last().unwrap();
7955
7956        // Edit the circle segment.
7957        let circle_ctor = CircleCtor {
7958            start: Point2d {
7959                x: Expr::Var(Number {
7960                    value: 10.0,
7961                    units: NumericSuffix::Mm,
7962                }),
7963                y: Expr::Var(Number {
7964                    value: 0.0,
7965                    units: NumericSuffix::Mm,
7966                }),
7967            },
7968            center: Point2d {
7969                x: Expr::Var(Number {
7970                    value: 3.0,
7971                    units: NumericSuffix::Mm,
7972                }),
7973                y: Expr::Var(Number {
7974                    value: 4.0,
7975                    units: NumericSuffix::Mm,
7976                }),
7977            },
7978            construction: None,
7979        };
7980        let segments = vec![ExistingSegmentCtor {
7981            id: circle,
7982            ctor: SegmentCtor::Circle(circle_ctor),
7983        }];
7984        let (src_delta, scene_delta) = frontend
7985            .edit_segments(&mock_ctx, version, sketch_id, segments)
7986            .await
7987            .unwrap();
7988        assert_eq!(
7989            src_delta.text.as_str(),
7990            "sketch001 = sketch(on = XY) {
7991  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7992}
7993"
7994        );
7995        assert_eq!(scene_delta.new_objects, vec![]);
7996        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7997
7998        ctx.close().await;
7999        mock_ctx.close().await;
8000    }
8001
8002    #[tokio::test(flavor = "multi_thread")]
8003    async fn test_delete_circle() {
8004        let initial_source = "sketch001 = sketch(on = XY) {
8005  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8006}
8007";
8008
8009        let program = Program::parse(initial_source).unwrap().0.unwrap();
8010        let mut frontend = FrontendState::new();
8011
8012        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8013        let mock_ctx = ExecutorContext::new_mock(None).await;
8014        let version = Version(0);
8015
8016        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8017        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8018        let sketch_id = sketch_object.id;
8019        let sketch = expect_sketch(sketch_object);
8020
8021        // The sketch should have 3 segments: start point, center point, and the circle.
8022        assert_eq!(sketch.segments.len(), 3);
8023        let circle_id = sketch.segments[2];
8024
8025        // Delete the circle.
8026        let (src_delta, scene_delta) = frontend
8027            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8028            .await
8029            .unwrap();
8030        assert_eq!(
8031            src_delta.text.as_str(),
8032            "sketch001 = sketch(on = XY) {
8033}
8034"
8035        );
8036        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8037        let new_sketch = expect_sketch(new_sketch_object);
8038        assert_eq!(new_sketch.segments.len(), 0);
8039
8040        ctx.close().await;
8041        mock_ctx.close().await;
8042    }
8043
8044    #[tokio::test(flavor = "multi_thread")]
8045    async fn test_edit_circle_via_point() {
8046        let initial_source = "sketch001 = sketch(on = XY) {
8047  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8048}
8049";
8050
8051        let program = Program::parse(initial_source).unwrap().0.unwrap();
8052        let mut frontend = FrontendState::new();
8053
8054        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8055        let mock_ctx = ExecutorContext::new_mock(None).await;
8056        let version = Version(0);
8057
8058        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8059        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8060        let sketch_id = sketch_object.id;
8061        let sketch = expect_sketch(sketch_object);
8062
8063        // Find the circle segment and its start point.
8064        let circle_id = sketch
8065            .segments
8066            .iter()
8067            .copied()
8068            .find(|seg_id| {
8069                matches!(
8070                    &frontend.scene_graph.objects[seg_id.0].kind,
8071                    ObjectKind::Segment {
8072                        segment: Segment::Circle(_)
8073                    }
8074                )
8075            })
8076            .expect("Expected a circle segment in sketch");
8077        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8078        let ObjectKind::Segment {
8079            segment: Segment::Circle(circle),
8080        } = &circle_object.kind
8081        else {
8082            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8083        };
8084        let start_point_id = circle.start;
8085
8086        // Edit the start point via SegmentCtor::Point.
8087        let segments = vec![ExistingSegmentCtor {
8088            id: start_point_id,
8089            ctor: SegmentCtor::Point(PointCtor {
8090                position: Point2d {
8091                    x: Expr::Var(Number {
8092                        value: 7.0,
8093                        units: NumericSuffix::Mm,
8094                    }),
8095                    y: Expr::Var(Number {
8096                        value: 1.0,
8097                        units: NumericSuffix::Mm,
8098                    }),
8099                },
8100            }),
8101        }];
8102        let (src_delta, _scene_delta) = frontend
8103            .edit_segments(&mock_ctx, version, sketch_id, segments)
8104            .await
8105            .unwrap();
8106        assert_eq!(
8107            src_delta.text.as_str(),
8108            "sketch001 = sketch(on = XY) {
8109  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8110}
8111"
8112        );
8113
8114        ctx.close().await;
8115        mock_ctx.close().await;
8116    }
8117
8118    #[tokio::test(flavor = "multi_thread")]
8119    async fn test_add_line_when_sketch_block_uses_variable() {
8120        let initial_source = "s = sketch(on = XY) {}
8121";
8122
8123        let program = Program::parse(initial_source).unwrap().0.unwrap();
8124
8125        let mut frontend = FrontendState::new();
8126
8127        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8128        let mock_ctx = ExecutorContext::new_mock(None).await;
8129        let version = Version(0);
8130
8131        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8132        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8133        let sketch_id = sketch_object.id;
8134
8135        let line_ctor = LineCtor {
8136            start: Point2d {
8137                x: Expr::Number(Number {
8138                    value: 0.0,
8139                    units: NumericSuffix::Mm,
8140                }),
8141                y: Expr::Number(Number {
8142                    value: 0.0,
8143                    units: NumericSuffix::Mm,
8144                }),
8145            },
8146            end: Point2d {
8147                x: Expr::Number(Number {
8148                    value: 10.0,
8149                    units: NumericSuffix::Mm,
8150                }),
8151                y: Expr::Number(Number {
8152                    value: 10.0,
8153                    units: NumericSuffix::Mm,
8154                }),
8155            },
8156            construction: None,
8157        };
8158        let segment = SegmentCtor::Line(line_ctor);
8159        let (src_delta, scene_delta) = frontend
8160            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8161            .await
8162            .unwrap();
8163        assert_eq!(
8164            src_delta.text.as_str(),
8165            "s = sketch(on = XY) {
8166  line(start = [0mm, 0mm], end = [10mm, 10mm])
8167}
8168"
8169        );
8170        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8171        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8172
8173        ctx.close().await;
8174        mock_ctx.close().await;
8175    }
8176
8177    #[tokio::test(flavor = "multi_thread")]
8178    async fn test_new_sketch_add_line_delete_sketch() {
8179        let program = Program::empty();
8180
8181        let mut frontend = FrontendState::new();
8182        frontend.program = program;
8183
8184        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8185        let mock_ctx = ExecutorContext::new_mock(None).await;
8186        let version = Version(0);
8187
8188        let sketch_args = SketchCtor {
8189            on: Plane::Default(PlaneName::Xy),
8190        };
8191        let (_src_delta, scene_delta, sketch_id) = frontend
8192            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8193            .await
8194            .unwrap();
8195        assert_eq!(sketch_id, ObjectId(1));
8196        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8197        let sketch_object = &scene_delta.new_graph.objects[1];
8198        assert_eq!(sketch_object.id, ObjectId(1));
8199        assert_eq!(
8200            sketch_object.kind,
8201            ObjectKind::Sketch(Sketch {
8202                args: SketchCtor {
8203                    on: Plane::Default(PlaneName::Xy)
8204                },
8205                plane: ObjectId(0),
8206                segments: vec![],
8207                constraints: vec![],
8208            })
8209        );
8210        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8211
8212        let line_ctor = LineCtor {
8213            start: Point2d {
8214                x: Expr::Number(Number {
8215                    value: 0.0,
8216                    units: NumericSuffix::Mm,
8217                }),
8218                y: Expr::Number(Number {
8219                    value: 0.0,
8220                    units: NumericSuffix::Mm,
8221                }),
8222            },
8223            end: Point2d {
8224                x: Expr::Number(Number {
8225                    value: 10.0,
8226                    units: NumericSuffix::Mm,
8227                }),
8228                y: Expr::Number(Number {
8229                    value: 10.0,
8230                    units: NumericSuffix::Mm,
8231                }),
8232            },
8233            construction: None,
8234        };
8235        let segment = SegmentCtor::Line(line_ctor);
8236        let (src_delta, scene_delta) = frontend
8237            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8238            .await
8239            .unwrap();
8240        assert_eq!(
8241            src_delta.text.as_str(),
8242            "sketch001 = sketch(on = XY) {
8243  line(start = [0mm, 0mm], end = [10mm, 10mm])
8244}
8245"
8246        );
8247        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8248
8249        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8250        assert_eq!(src_delta.text.as_str(), "");
8251        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8252
8253        ctx.close().await;
8254        mock_ctx.close().await;
8255    }
8256
8257    #[tokio::test(flavor = "multi_thread")]
8258    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8259        let initial_source = "s = sketch(on = XY) {}
8260";
8261
8262        let program = Program::parse(initial_source).unwrap().0.unwrap();
8263
8264        let mut frontend = FrontendState::new();
8265
8266        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8267        let version = Version(0);
8268
8269        frontend.hack_set_program(&ctx, program).await.unwrap();
8270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8271        let sketch_id = sketch_object.id;
8272
8273        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8274        assert_eq!(src_delta.text.as_str(), "");
8275        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8276
8277        ctx.close().await;
8278    }
8279
8280    #[tokio::test(flavor = "multi_thread")]
8281    async fn test_delete_sketch_after_comment() {
8282        let initial_source = "sketch001 = sketch(on = XZ) {
8283}
8284";
8285
8286        let program = Program::parse(initial_source).unwrap().0.unwrap();
8287        let mut frontend = FrontendState::new();
8288
8289        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8290        let version = Version(0);
8291
8292        frontend.hack_set_program(&ctx, program).await.unwrap();
8293        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8294        let sketch_id = sketch_object.id;
8295        let original_source = sketch_object.source.clone();
8296
8297        let commented_source = "// test 1
8298sketch001 = sketch(on = XZ) {
8299}
8300";
8301        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8302        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8303
8304        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8305        assert_eq!(cached_sketch_object.source, original_source);
8306
8307        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8308        assert!(
8309            !src_delta.text.contains("sketch001"),
8310            "sketch was not deleted: {}",
8311            src_delta.text
8312        );
8313        // The leading line comment must survive deletion.
8314        assert_eq!(src_delta.text.as_str(), "// test 1\n");
8315        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8316
8317        ctx.close().await;
8318    }
8319
8320    #[tokio::test(flavor = "multi_thread")]
8321    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8322        let initial_source = "sketch001 = sketch(on = XZ) {
8323}
8324foo = 1
8325";
8326
8327        let program = Program::parse(initial_source).unwrap().0.unwrap();
8328        let mut frontend = FrontendState::new();
8329
8330        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8331        let version = Version(0);
8332
8333        frontend.hack_set_program(&ctx, program).await.unwrap();
8334        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8335        let sketch_id = sketch_object.id;
8336
8337        let commented_source = "// keep me
8338sketch001 = sketch(on = XZ) {
8339}
8340foo = 1
8341";
8342        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8343        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8344
8345        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8346        // The leading comment should remain, now attached to the following body item.
8347        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8348
8349        ctx.close().await;
8350    }
8351
8352    #[tokio::test(flavor = "multi_thread")]
8353    async fn test_delete_segment_preserves_pre_comment() {
8354        let initial_source = "\
8355sketch(on = XY) {
8356  point(at = [var 1, var 2])
8357  // describe the middle point
8358  point(at = [var 3, var 4])
8359  point(at = [var 5, var 6])
8360}
8361";
8362
8363        let program = Program::parse(initial_source).unwrap().0.unwrap();
8364        let mut frontend = FrontendState::new();
8365
8366        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8367        let mock_ctx = ExecutorContext::new_mock(None).await;
8368        let version = Version(0);
8369
8370        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8371        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8372        let sketch_id = sketch_object.id;
8373        let sketch = expect_sketch(sketch_object);
8374
8375        let middle_point_id = *sketch.segments.get(1).unwrap();
8376
8377        let (src_delta, _scene_delta) = frontend
8378            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8379            .await
8380            .unwrap();
8381        // The line comment on the line above the deleted point must be preserved.
8382        // It is reattached to the next surviving body item.
8383        assert_eq!(
8384            src_delta.text.as_str(),
8385            "\
8386sketch(on = XY) {
8387  point(at = [var 1, var 2])
8388  // describe the middle point
8389  point(at = [var 5, var 6])
8390}
8391"
8392        );
8393
8394        ctx.close().await;
8395        mock_ctx.close().await;
8396    }
8397
8398    #[tokio::test(flavor = "multi_thread")]
8399    async fn test_delete_last_segment_preserves_pre_comment() {
8400        let initial_source = "\
8401sketch(on = XY) {
8402  point(at = [var 1, var 2])
8403  // describe the trailing point
8404  point(at = [var 3, var 4])
8405}
8406";
8407
8408        let program = Program::parse(initial_source).unwrap().0.unwrap();
8409        let mut frontend = FrontendState::new();
8410
8411        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8412        let mock_ctx = ExecutorContext::new_mock(None).await;
8413        let version = Version(0);
8414
8415        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8416        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8417        let sketch_id = sketch_object.id;
8418        let sketch = expect_sketch(sketch_object);
8419
8420        let last_point_id = *sketch.segments.last().unwrap();
8421
8422        let (src_delta, _scene_delta) = frontend
8423            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8424            .await
8425            .unwrap();
8426        // No following item to attach to; the comment is kept inside the sketch
8427        // block as trailing non-code metadata so the user does not lose it.
8428        assert_eq!(
8429            src_delta.text.as_str(),
8430            "\
8431sketch(on = XY) {
8432  point(at = [var 1, var 2])
8433  // describe the trailing point
8434}
8435"
8436        );
8437
8438        ctx.close().await;
8439        mock_ctx.close().await;
8440    }
8441
8442    #[tokio::test(flavor = "multi_thread")]
8443    async fn test_delete_segment_drops_inline_trailing_comment() {
8444        let initial_source = "\
8445sketch(on = XY) {
8446  point(at = [var 1, var 2])
8447  point(at = [var 3, var 4]) // same-line note that gets dropped
8448  point(at = [var 5, var 6])
8449}
8450";
8451
8452        let program = Program::parse(initial_source).unwrap().0.unwrap();
8453        let mut frontend = FrontendState::new();
8454
8455        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8456        let mock_ctx = ExecutorContext::new_mock(None).await;
8457        let version = Version(0);
8458
8459        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8460        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8461        let sketch_id = sketch_object.id;
8462        let sketch = expect_sketch(sketch_object);
8463
8464        let middle_point_id = *sketch.segments.get(1).unwrap();
8465
8466        let (src_delta, _scene_delta) = frontend
8467            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8468            .await
8469            .unwrap();
8470        // The same-line trailing comment is removed along with the deleted code.
8471        assert!(
8472            !src_delta.text.contains("same-line note"),
8473            "inline comment should have been removed: {}",
8474            src_delta.text
8475        );
8476
8477        ctx.close().await;
8478        mock_ctx.close().await;
8479    }
8480
8481    #[tokio::test(flavor = "multi_thread")]
8482    async fn test_delete_segments_preserves_block_comments_across_positions() {
8483        // One test exercising several `delete_body_item_preserving_pre_comments`
8484        // branches at once with `/* ... */` block comments:
8485        //   - first point: leading block comment must migrate to the next item.
8486        //   - first point: same-line trailing block comment must be dropped.
8487        //   - middle point: leading block comment must stay attached after migration.
8488        //   - last point: leading block comment, with no surviving next item,
8489        //     must be converted into a trailing NonCodeNode.
8490        let initial_source = "\
8491sketch(on = XY) {
8492  /* above first - moves to middle */
8493  point(at = [var 1, var 2]) /* same-line on first - dropped */
8494  /* above middle - stays */
8495  point(at = [var 3, var 4])
8496  /* above last - moves to trailing meta */
8497  point(at = [var 5, var 6])
8498}
8499";
8500
8501        let program = Program::parse(initial_source).unwrap().0.unwrap();
8502        let mut frontend = FrontendState::new();
8503
8504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8505        let mock_ctx = ExecutorContext::new_mock(None).await;
8506        let version = Version(0);
8507
8508        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8509        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8510        let sketch_id = sketch_object.id;
8511        let sketch = expect_sketch(sketch_object);
8512
8513        let first_point_id = *sketch.segments.first().unwrap();
8514        let last_point_id = *sketch.segments.last().unwrap();
8515
8516        let (src_delta, _scene_delta) = frontend
8517            .delete_objects(
8518                &mock_ctx,
8519                version,
8520                sketch_id,
8521                Vec::new(),
8522                vec![first_point_id, last_point_id],
8523            )
8524            .await
8525            .unwrap();
8526        assert_eq!(
8527            src_delta.text.as_str(),
8528            "\
8529sketch(on = XY) {
8530  /* above first - moves to middle */
8531  /* above middle - stays */
8532  point(at = [var 3, var 4])
8533  /* above last - moves to trailing meta */
8534}
8535"
8536        );
8537
8538        ctx.close().await;
8539        mock_ctx.close().await;
8540    }
8541
8542    #[tokio::test(flavor = "multi_thread")]
8543    async fn test_edit_line_when_editing_its_start_point() {
8544        let initial_source = "\
8545sketch(on = XY) {
8546  line(start = [var 1, var 2], end = [var 3, var 4])
8547}
8548";
8549
8550        let program = Program::parse(initial_source).unwrap().0.unwrap();
8551
8552        let mut frontend = FrontendState::new();
8553
8554        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8555        let mock_ctx = ExecutorContext::new_mock(None).await;
8556        let version = Version(0);
8557
8558        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8559        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8560        let sketch_id = sketch_object.id;
8561        let sketch = expect_sketch(sketch_object);
8562
8563        let point_id = *sketch.segments.first().unwrap();
8564
8565        let point_ctor = PointCtor {
8566            position: Point2d {
8567                x: Expr::Var(Number {
8568                    value: 5.0,
8569                    units: NumericSuffix::Inch,
8570                }),
8571                y: Expr::Var(Number {
8572                    value: 6.0,
8573                    units: NumericSuffix::Inch,
8574                }),
8575            },
8576        };
8577        let segments = vec![ExistingSegmentCtor {
8578            id: point_id,
8579            ctor: SegmentCtor::Point(point_ctor),
8580        }];
8581        let (src_delta, scene_delta) = frontend
8582            .edit_segments(&mock_ctx, version, sketch_id, segments)
8583            .await
8584            .unwrap();
8585        assert_eq!(
8586            src_delta.text.as_str(),
8587            "\
8588sketch(on = XY) {
8589  line(start = [var 5in, var 6in], end = [var 3, var 4])
8590}
8591"
8592        );
8593        assert_eq!(scene_delta.new_objects, vec![]);
8594        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8595
8596        ctx.close().await;
8597        mock_ctx.close().await;
8598    }
8599
8600    #[tokio::test(flavor = "multi_thread")]
8601    async fn test_edit_line_when_editing_its_end_point() {
8602        let initial_source = "\
8603sketch(on = XY) {
8604  line(start = [var 1, var 2], end = [var 3, var 4])
8605}
8606";
8607
8608        let program = Program::parse(initial_source).unwrap().0.unwrap();
8609
8610        let mut frontend = FrontendState::new();
8611
8612        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8613        let mock_ctx = ExecutorContext::new_mock(None).await;
8614        let version = Version(0);
8615
8616        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8617        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8618        let sketch_id = sketch_object.id;
8619        let sketch = expect_sketch(sketch_object);
8620        let point_id = *sketch.segments.get(1).unwrap();
8621
8622        let point_ctor = PointCtor {
8623            position: Point2d {
8624                x: Expr::Var(Number {
8625                    value: 5.0,
8626                    units: NumericSuffix::Inch,
8627                }),
8628                y: Expr::Var(Number {
8629                    value: 6.0,
8630                    units: NumericSuffix::Inch,
8631                }),
8632            },
8633        };
8634        let segments = vec![ExistingSegmentCtor {
8635            id: point_id,
8636            ctor: SegmentCtor::Point(point_ctor),
8637        }];
8638        let (src_delta, scene_delta) = frontend
8639            .edit_segments(&mock_ctx, version, sketch_id, segments)
8640            .await
8641            .unwrap();
8642        assert_eq!(
8643            src_delta.text.as_str(),
8644            "\
8645sketch(on = XY) {
8646  line(start = [var 1, var 2], end = [var 5in, var 6in])
8647}
8648"
8649        );
8650        assert_eq!(scene_delta.new_objects, vec![]);
8651        assert_eq!(
8652            scene_delta.new_graph.objects.len(),
8653            5,
8654            "{:#?}",
8655            scene_delta.new_graph.objects
8656        );
8657
8658        ctx.close().await;
8659        mock_ctx.close().await;
8660    }
8661
8662    #[tokio::test(flavor = "multi_thread")]
8663    async fn test_edit_line_with_coincident_feedback() {
8664        let initial_source = "\
8665sketch(on = XY) {
8666  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8667  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8668  fixed([line1.start, [0, 0]])
8669  coincident([line1.end, line2.start])
8670  equalLength([line1, line2])
8671}
8672";
8673
8674        let program = Program::parse(initial_source).unwrap().0.unwrap();
8675
8676        let mut frontend = FrontendState::new();
8677
8678        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8679        let mock_ctx = ExecutorContext::new_mock(None).await;
8680        let version = Version(0);
8681
8682        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8684        let sketch_id = sketch_object.id;
8685        let sketch = expect_sketch(sketch_object);
8686        let line2_end_id = *sketch.segments.get(4).unwrap();
8687
8688        let segments = vec![ExistingSegmentCtor {
8689            id: line2_end_id,
8690            ctor: SegmentCtor::Point(PointCtor {
8691                position: Point2d {
8692                    x: Expr::Var(Number {
8693                        value: 9.0,
8694                        units: NumericSuffix::None,
8695                    }),
8696                    y: Expr::Var(Number {
8697                        value: 10.0,
8698                        units: NumericSuffix::None,
8699                    }),
8700                },
8701            }),
8702        }];
8703        let (src_delta, scene_delta) = frontend
8704            .edit_segments(&mock_ctx, version, sketch_id, segments)
8705            .await
8706            .unwrap();
8707        assert_eq!(
8708            src_delta.text.as_str(),
8709            "\
8710sketch(on = XY) {
8711  line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8712  line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8713  fixed([line1.start, [0, 0]])
8714  coincident([line1.end, line2.start])
8715  equalLength([line1, line2])
8716}
8717"
8718        );
8719        assert_eq!(
8720            scene_delta.new_graph.objects.len(),
8721            11,
8722            "{:#?}",
8723            scene_delta.new_graph.objects
8724        );
8725
8726        ctx.close().await;
8727        mock_ctx.close().await;
8728    }
8729
8730    #[tokio::test(flavor = "multi_thread")]
8731    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8732        let initial_source = "\
8733sketch(on = XY) {
8734  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8735  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8736  fixed([line1.start, [0, 0]])
8737  coincident([line1.end, line2.start])
8738  equalLength([line1, line2])
8739}
8740";
8741
8742        let program = Program::parse(initial_source).unwrap().0.unwrap();
8743        let mut frontend = FrontendState::new();
8744        let mock_ctx = ExecutorContext::new_mock(None).await;
8745        let version = Version(0);
8746
8747        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8748        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8749        let sketch_id = sketch_object.id;
8750        let sketch = expect_sketch(sketch_object);
8751        let line2_end_id = *sketch.segments.get(4).unwrap();
8752
8753        let segments = vec![ExistingSegmentCtor {
8754            id: line2_end_id,
8755            ctor: SegmentCtor::Point(PointCtor {
8756                position: Point2d {
8757                    x: Expr::Var(Number {
8758                        value: 9.0,
8759                        units: NumericSuffix::None,
8760                    }),
8761                    y: Expr::Var(Number {
8762                        value: 10.0,
8763                        units: NumericSuffix::None,
8764                    }),
8765                },
8766            }),
8767        }];
8768        let (edited_source, _) = frontend
8769            .edit_segments(&mock_ctx, version, sketch_id, segments)
8770            .await
8771            .unwrap();
8772
8773        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8774        assert_eq!(mock_source.text, edited_source.text);
8775
8776        mock_ctx.close().await;
8777    }
8778
8779    /// Preview segment edits should return solved geometry without persisting
8780    /// solver feedback to KCL.
8781    #[tokio::test(flavor = "multi_thread")]
8782    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8783        let initial_source = "\
8784sketch(on = XY) {
8785  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8786  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8787  fixed([line1.start, [0, 0]])
8788  coincident([line1.end, line2.start])
8789  equalLength([line1, line2])
8790}
8791";
8792
8793        let program = Program::parse(initial_source).unwrap().0.unwrap();
8794        let mut frontend = FrontendState::new();
8795        let mock_ctx = ExecutorContext::new_mock(None).await;
8796        let version = Version(0);
8797
8798        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8800        let sketch_id = sketch_object.id;
8801        let sketch = expect_sketch(sketch_object);
8802        let line2_end_id = *sketch.segments.get(4).unwrap();
8803
8804        let segments = vec![ExistingSegmentCtor {
8805            id: line2_end_id,
8806            ctor: SegmentCtor::Point(PointCtor {
8807                position: Point2d {
8808                    x: Expr::Var(Number {
8809                        value: 9.0,
8810                        units: NumericSuffix::None,
8811                    }),
8812                    y: Expr::Var(Number {
8813                        value: 10.0,
8814                        units: NumericSuffix::None,
8815                    }),
8816                },
8817            }),
8818        }];
8819        let (preview_source, preview_delta) = frontend
8820            .edit_segments_with_options(
8821                &mock_ctx,
8822                version,
8823                sketch_id,
8824                segments,
8825                EditSegmentsOptions {
8826                    anchor_segment_ids: Some(vec![line2_end_id]),
8827                    drag_anchors: Vec::new(),
8828                    commit_solved_initial_guesses: false,
8829                },
8830            )
8831            .await
8832            .unwrap();
8833
8834        assert!(
8835            !preview_delta.exec_outcome.var_solutions.is_empty(),
8836            "preview solve should still solve and return geometry feedback"
8837        );
8838        assert!(
8839            preview_source
8840                .text
8841                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8842        );
8843        assert!(
8844            preview_source
8845                .text
8846                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8847        );
8848
8849        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8850        assert_eq!(mock_source.text, preview_source.text);
8851
8852        mock_ctx.close().await;
8853    }
8854
8855    #[tokio::test(flavor = "multi_thread")]
8856    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8857        let initial_source = "\
8858sketch(on = XY) {
8859  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8860}
8861";
8862
8863        let program = Program::parse(initial_source).unwrap().0.unwrap();
8864        let mut frontend = FrontendState::new();
8865        let mock_ctx = ExecutorContext::new_mock(None).await;
8866        let version = Version(0);
8867
8868        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8869        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8870        let sketch_id = sketch_object.id;
8871        let sketch = expect_sketch(sketch_object);
8872        let line_end_id = *sketch.segments.get(1).unwrap();
8873
8874        let constraint = Constraint::Fixed(Fixed {
8875            points: vec![FixedPoint {
8876                point: line_end_id,
8877                position: Point2d {
8878                    x: Number {
8879                        value: 20.0,
8880                        units: NumericSuffix::Mm,
8881                    },
8882                    y: Number {
8883                        value: 0.0,
8884                        units: NumericSuffix::Mm,
8885                    },
8886                },
8887            }],
8888        });
8889        let (constraint_source, _) = frontend
8890            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8891            .await
8892            .unwrap();
8893
8894        assert!(
8895            constraint_source
8896                .text
8897                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8898            "{}",
8899            constraint_source.text
8900        );
8901        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8902        assert_eq!(mock_source.text, constraint_source.text);
8903
8904        mock_ctx.close().await;
8905    }
8906
8907    #[test]
8908    fn test_no_solver_feedback_preserves_original_source() {
8909        let initial_source = "\
8910@settings(defaultLengthUnit = in, kclVersion = 2.0)
8911cylinder = startSketchOn(XY)
8912    |> circle(center= [0, 0], radius= 22)
8913    |> extrude(length = 14)
8914";
8915        let mut frontend = FrontendState::new();
8916        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8917        let outcome = ExecOutcome {
8918            variables: Default::default(),
8919            operations: Default::default(),
8920            artifact_graph: Default::default(),
8921            scene_objects: Default::default(),
8922            source_range_to_object: Default::default(),
8923            var_solutions: Default::default(),
8924            issues: Default::default(),
8925            filenames: Default::default(),
8926            default_planes: Default::default(),
8927        };
8928
8929        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8930
8931        assert_eq!(source_delta.text, initial_source);
8932    }
8933
8934    /// Explicit drag anchors should limit which edited points become temporary
8935    /// fixed constraints.
8936    #[tokio::test(flavor = "multi_thread")]
8937    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8938        let initial_source = "\
8939sketch(on = XY) {
8940  point1 = point(at = [var 0mm, var 0mm])
8941  point2 = point(at = [var 0mm, var 0mm])
8942  coincident([point1, point2])
8943}
8944";
8945
8946        let program = Program::parse(initial_source).unwrap().0.unwrap();
8947        let mut frontend = FrontendState::new();
8948        let mock_ctx = ExecutorContext::new_mock(None).await;
8949        let version = Version(0);
8950
8951        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8952        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8953        let sketch_id = sketch_object.id;
8954        let sketch = expect_sketch(sketch_object);
8955        let point1_id = sketch.segments[0];
8956        let point2_id = sketch.segments[1];
8957
8958        let segments = vec![
8959            ExistingSegmentCtor {
8960                id: point1_id,
8961                ctor: SegmentCtor::Point(PointCtor {
8962                    position: point_expr_mm(10.0, 0.0),
8963                }),
8964            },
8965            ExistingSegmentCtor {
8966                id: point2_id,
8967                ctor: SegmentCtor::Point(PointCtor {
8968                    position: point_expr_mm(100.0, 0.0),
8969                }),
8970            },
8971        ];
8972        let (_, scene_delta) = frontend
8973            .edit_segments_with_options(
8974                &mock_ctx,
8975                version,
8976                sketch_id,
8977                segments,
8978                EditSegmentsOptions {
8979                    anchor_segment_ids: Some(vec![point1_id]),
8980                    drag_anchors: Vec::new(),
8981                    commit_solved_initial_guesses: true,
8982                },
8983            )
8984            .await
8985            .unwrap();
8986
8987        assert_point_position_close(
8988            point_position(&scene_delta.new_graph, point1_id),
8989            point_number_mm(10.0, 0.0),
8990        );
8991        assert_point_position_close(
8992            point_position(&scene_delta.new_graph, point2_id),
8993            point_number_mm(10.0, 0.0),
8994        );
8995
8996        mock_ctx.close().await;
8997    }
8998
8999    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9000    /// for every SketchVar whose initial NumericLiteral has the given value.
9001    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9002        use std::cell::RefCell;
9003        struct Collector {
9004            target: f64,
9005            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9006        }
9007        impl<'a> crate::walk::Visitor<'a> for &Collector {
9008            type Error = crate::front::Error;
9009            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9010                if let crate::walk::Node::SketchVar(sketch_var) = node
9011                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9012                    && (initial.value - self.target).abs() < 1e-9
9013                {
9014                    self.out
9015                        .borrow_mut()
9016                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9017                }
9018                for child in node.children().iter() {
9019                    if !child.visit(*self)? {
9020                        return Ok(false);
9021                    }
9022                }
9023                Ok(true)
9024            }
9025        }
9026        let collector = Collector {
9027            target: value,
9028            out: Default::default(),
9029        };
9030        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9031        collector.out.into_inner()
9032    }
9033
9034    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9035    /// for every SketchVar (including bare `var`).
9036    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9037        use std::cell::RefCell;
9038        struct Collector {
9039            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9040        }
9041        impl<'a> crate::walk::Visitor<'a> for &Collector {
9042            type Error = crate::front::Error;
9043            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9044                if let crate::walk::Node::SketchVar(sketch_var) = node
9045                    && let Some(node_path) = &sketch_var.node_path
9046                {
9047                    self.out
9048                        .borrow_mut()
9049                        .push((SourceRange::from(sketch_var), node_path.clone()));
9050                }
9051                for child in node.children().iter() {
9052                    if !child.visit(*self)? {
9053                        return Ok(false);
9054                    }
9055                }
9056                Ok(true)
9057            }
9058        }
9059        let collector = Collector {
9060            out: Default::default(),
9061        };
9062        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9063        collector.out.into_inner()
9064    }
9065
9066    fn empty_exec_outcome_with_var_solutions(
9067        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9068    ) -> ExecOutcome {
9069        ExecOutcome {
9070            variables: Default::default(),
9071            operations: Default::default(),
9072            artifact_graph: Default::default(),
9073            scene_objects: Default::default(),
9074            source_range_to_object: Default::default(),
9075            var_solutions,
9076            issues: Default::default(),
9077            filenames: Default::default(),
9078            default_planes: Default::default(),
9079        }
9080    }
9081
9082    /// Happy path: commit a var solution to a `var N` inside a sketch block
9083    /// using a correct NodePath. Confirms the node-path code path produces the
9084    /// expected source mutation.
9085    #[test]
9086    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9087        let initial_source = "\
9088sketch(on = XY) {
9089  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9090}
9091";
9092        let program = Program::parse(initial_source).unwrap().0.unwrap();
9093        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9094        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9095        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9096
9097        let mut frontend = FrontendState::new();
9098        frontend.program = program;
9099
9100        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9101            literal_range,
9102            Some(node_path),
9103            Number {
9104                value: 25.0,
9105                units: NumericSuffix::Mm,
9106            },
9107        )]);
9108
9109        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9110
9111        assert_eq!(
9112            source_delta.text,
9113            "\
9114sketch(on = XY) {
9115  line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9116}
9117",
9118        );
9119    }
9120
9121    /// Whitespace inserted earlier in the source shifts the original SketchVar
9122    /// SourceRange. With NodePath propagation the commit should still target
9123    /// the right `var`. We simulate this by collecting node_paths against a
9124    /// "compact" source, then loading the frontend with a "padded" source
9125    /// (whose byte offsets differ), and feeding the original (now stale)
9126    /// source range plus the correct node_path back into the commit.
9127    #[test]
9128    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9129        let compact_source = "\
9130sketch(on = XY) {
9131  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9132}
9133";
9134        let padded_source = "\
9135// added comment\n// added comment\n\nsketch(on = XY) {
9136  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9137}
9138";
9139        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9140        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9141
9142        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9143            .into_iter()
9144            .next()
9145            .expect("expected `var 10mm` in compact source");
9146        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9147            .into_iter()
9148            .next()
9149            .expect("expected `var 10mm` in padded source");
9150
9151        assert_ne!(
9152            compact_match.0, padded_match.0,
9153            "byte offsets must differ for this test to be meaningful"
9154        );
9155        assert_eq!(
9156            compact_match.1, padded_match.1,
9157            "node paths must agree across whitespace; that's the whole point of NodePath",
9158        );
9159
9160        let mut frontend = FrontendState::new();
9161        frontend.program = padded_program;
9162
9163        // Stale source range from the compact source + correct node_path.
9164        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9165            compact_match.0,
9166            Some(compact_match.1),
9167            Number {
9168                value: 30.0,
9169                units: NumericSuffix::Mm,
9170            },
9171        )]);
9172
9173        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9174
9175        assert_eq!(
9176            source_delta.text,
9177            "\
9178// added comment
9179// added comment
9180
9181sketch(on = XY) {
9182  line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9183}
9184",
9185        );
9186    }
9187
9188    /// When multiple `var` declarations exist and the stale source range
9189    /// happens to land on a *different* var, the node_path must take
9190    /// precedence and the right var gets updated.
9191    #[test]
9192    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9193        let initial_source = "\
9194sketch(on = XY) {
9195  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9196}
9197";
9198        let program = Program::parse(initial_source).unwrap().0.unwrap();
9199
9200        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9201            .into_iter()
9202            .next()
9203            .expect("expected `var 10mm`");
9204        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9205            .into_iter()
9206            .next()
9207            .expect("expected `var 20mm`");
9208
9209        let mut frontend = FrontendState::new();
9210        frontend.program = program;
9211
9212        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9213        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9214            var_20.0,
9215            Some(var_10.1),
9216            Number {
9217                value: 33.0,
9218                units: NumericSuffix::Mm,
9219            },
9220        )]);
9221
9222        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9223
9224        assert_eq!(
9225            source_delta.text,
9226            "\
9227sketch(on = XY) {
9228  line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9229}
9230",
9231        );
9232    }
9233
9234    /// Bare `var` (no initial literal) is only locatable via node_path. With
9235    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9236    /// solver solution should fill the initial value in. The
9237    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9238    /// is gated as an experimental feature; without it the re-parse of the
9239    /// recast source rejects bare `var` declarations.
9240    #[test]
9241    fn test_commit_var_solution_writes_back_into_bare_var() {
9242        let initial_source = "\
9243@settings(experimentalFeatures = allow, kclVersion = 2.0)
9244sketch(on = XY) {
9245  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9246}
9247";
9248        let program = Program::parse(initial_source).unwrap().0.unwrap();
9249
9250        // Pick the first bare `var`; collect_all_sketch_vars returns every
9251        // SketchVar, including bare ones.
9252        let bare = collect_all_sketch_vars(&program)
9253            .into_iter()
9254            .find(|(range, _)| {
9255                // The bare `var` is exactly the 3 characters "var".
9256                range.end() - range.start() == 3
9257            })
9258            .expect("expected at least one bare `var`");
9259
9260        let mut frontend = FrontendState::new();
9261        frontend.program = program;
9262
9263        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9264            bare.0,
9265            Some(bare.1),
9266            Number {
9267                value: 7.0,
9268                units: NumericSuffix::Mm,
9269            },
9270        )]);
9271
9272        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9273
9274        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9275        // written as an explicit suffix so the bare var commits with units.
9276        // The recast adds a blank line after the `@settings` annotation.
9277        assert_eq!(
9278            source_delta.text,
9279            "\
9280@settings(experimentalFeatures = allow, kclVersion = 2.0)
9281
9282sketch(on = XY) {
9283  line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9284}
9285",
9286        );
9287    }
9288
9289    #[tokio::test(flavor = "multi_thread")]
9290    async fn test_delete_point_without_var() {
9291        let initial_source = "\
9292sketch(on = XY) {
9293  point(at = [var 1, var 2])
9294  point(at = [var 3, var 4])
9295  point(at = [var 5, var 6])
9296}
9297";
9298
9299        let program = Program::parse(initial_source).unwrap().0.unwrap();
9300
9301        let mut frontend = FrontendState::new();
9302
9303        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9304        let mock_ctx = ExecutorContext::new_mock(None).await;
9305        let version = Version(0);
9306
9307        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9308        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9309        let sketch_id = sketch_object.id;
9310        let sketch = expect_sketch(sketch_object);
9311
9312        let point_id = *sketch.segments.get(1).unwrap();
9313
9314        let (src_delta, scene_delta) = frontend
9315            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9316            .await
9317            .unwrap();
9318        assert_eq!(
9319            src_delta.text.as_str(),
9320            "\
9321sketch(on = XY) {
9322  point(at = [var 1, var 2])
9323  point(at = [var 5, var 6])
9324}
9325"
9326        );
9327        assert_eq!(scene_delta.new_objects, vec![]);
9328        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9329
9330        ctx.close().await;
9331        mock_ctx.close().await;
9332    }
9333
9334    #[tokio::test(flavor = "multi_thread")]
9335    async fn test_delete_point_with_var() {
9336        let initial_source = "\
9337sketch(on = XY) {
9338  point(at = [var 1, var 2])
9339  point1 = point(at = [var 3, var 4])
9340  point(at = [var 5, var 6])
9341}
9342";
9343
9344        let program = Program::parse(initial_source).unwrap().0.unwrap();
9345
9346        let mut frontend = FrontendState::new();
9347
9348        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9349        let mock_ctx = ExecutorContext::new_mock(None).await;
9350        let version = Version(0);
9351
9352        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9353        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9354        let sketch_id = sketch_object.id;
9355        let sketch = expect_sketch(sketch_object);
9356
9357        let point_id = *sketch.segments.get(1).unwrap();
9358
9359        let (src_delta, scene_delta) = frontend
9360            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9361            .await
9362            .unwrap();
9363        assert_eq!(
9364            src_delta.text.as_str(),
9365            "\
9366sketch(on = XY) {
9367  point(at = [var 1, var 2])
9368  point(at = [var 5, var 6])
9369}
9370"
9371        );
9372        assert_eq!(scene_delta.new_objects, vec![]);
9373        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9374
9375        ctx.close().await;
9376        mock_ctx.close().await;
9377    }
9378
9379    #[tokio::test(flavor = "multi_thread")]
9380    async fn test_delete_multiple_points() {
9381        let initial_source = "\
9382sketch(on = XY) {
9383  point(at = [var 1, var 2])
9384  point1 = point(at = [var 3, var 4])
9385  point(at = [var 5, var 6])
9386}
9387";
9388
9389        let program = Program::parse(initial_source).unwrap().0.unwrap();
9390
9391        let mut frontend = FrontendState::new();
9392
9393        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9394        let mock_ctx = ExecutorContext::new_mock(None).await;
9395        let version = Version(0);
9396
9397        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9398        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9399        let sketch_id = sketch_object.id;
9400
9401        let sketch = expect_sketch(sketch_object);
9402
9403        let point1_id = *sketch.segments.first().unwrap();
9404        let point2_id = *sketch.segments.get(1).unwrap();
9405
9406        let (src_delta, scene_delta) = frontend
9407            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9408            .await
9409            .unwrap();
9410        assert_eq!(
9411            src_delta.text.as_str(),
9412            "\
9413sketch(on = XY) {
9414  point(at = [var 5, var 6])
9415}
9416"
9417        );
9418        assert_eq!(scene_delta.new_objects, vec![]);
9419        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9420
9421        ctx.close().await;
9422        mock_ctx.close().await;
9423    }
9424
9425    #[tokio::test(flavor = "multi_thread")]
9426    async fn test_delete_coincident_constraint() {
9427        let initial_source = "\
9428sketch(on = XY) {
9429  point1 = point(at = [var 1, var 2])
9430  point2 = point(at = [var 3, var 4])
9431  coincident([point1, point2])
9432  point(at = [var 5, var 6])
9433}
9434";
9435
9436        let program = Program::parse(initial_source).unwrap().0.unwrap();
9437
9438        let mut frontend = FrontendState::new();
9439
9440        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9441        let mock_ctx = ExecutorContext::new_mock(None).await;
9442        let version = Version(0);
9443
9444        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9445        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9446        let sketch_id = sketch_object.id;
9447        let sketch = expect_sketch(sketch_object);
9448
9449        let coincident_id = *sketch.constraints.first().unwrap();
9450
9451        let (src_delta, scene_delta) = frontend
9452            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9453            .await
9454            .unwrap();
9455        assert_eq!(
9456            src_delta.text.as_str(),
9457            "\
9458sketch(on = XY) {
9459  point1 = point(at = [var 1, var 2])
9460  point2 = point(at = [var 3, var 4])
9461  point(at = [var 5, var 6])
9462}
9463"
9464        );
9465        assert_eq!(scene_delta.new_objects, vec![]);
9466        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9467
9468        ctx.close().await;
9469        mock_ctx.close().await;
9470    }
9471
9472    #[tokio::test(flavor = "multi_thread")]
9473    async fn test_delete_line_cascades_to_coincident_constraint() {
9474        let initial_source = "\
9475sketch(on = XY) {
9476  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9477  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9478  coincident([line1.end, line2.start])
9479}
9480";
9481
9482        let program = Program::parse(initial_source).unwrap().0.unwrap();
9483
9484        let mut frontend = FrontendState::new();
9485
9486        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9487        let mock_ctx = ExecutorContext::new_mock(None).await;
9488        let version = Version(0);
9489
9490        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9491        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9492        let sketch_id = sketch_object.id;
9493        let sketch = expect_sketch(sketch_object);
9494        let line_id = *sketch.segments.get(5).unwrap();
9495
9496        let (src_delta, scene_delta) = frontend
9497            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9498            .await
9499            .unwrap();
9500        assert_eq!(
9501            src_delta.text.as_str(),
9502            "\
9503sketch(on = XY) {
9504  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9505}
9506"
9507        );
9508        assert_eq!(
9509            scene_delta.new_graph.objects.len(),
9510            5,
9511            "{:#?}",
9512            scene_delta.new_graph.objects
9513        );
9514
9515        ctx.close().await;
9516        mock_ctx.close().await;
9517    }
9518
9519    #[tokio::test(flavor = "multi_thread")]
9520    async fn test_delete_line_cascades_to_distance_constraint() {
9521        let initial_source = "\
9522sketch(on = XY) {
9523  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9524  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9525  distance([line1.end, line2.start]) == 10mm
9526}
9527";
9528
9529        let program = Program::parse(initial_source).unwrap().0.unwrap();
9530
9531        let mut frontend = FrontendState::new();
9532
9533        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9534        let mock_ctx = ExecutorContext::new_mock(None).await;
9535        let version = Version(0);
9536
9537        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9538        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9539        let sketch_id = sketch_object.id;
9540        let sketch = expect_sketch(sketch_object);
9541        let line_id = *sketch.segments.get(5).unwrap();
9542
9543        let (src_delta, scene_delta) = frontend
9544            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9545            .await
9546            .unwrap();
9547        assert_eq!(
9548            src_delta.text.as_str(),
9549            "\
9550sketch(on = XY) {
9551  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9552}
9553"
9554        );
9555        assert_eq!(
9556            scene_delta.new_graph.objects.len(),
9557            5,
9558            "{:#?}",
9559            scene_delta.new_graph.objects
9560        );
9561
9562        ctx.close().await;
9563        mock_ctx.close().await;
9564    }
9565
9566    #[tokio::test(flavor = "multi_thread")]
9567    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9568        let initial_source = "\
9569sketch(on = XY) {
9570  point1 = point(at = [var 1, var 2])
9571  point2 = point(at = [var 3, var 4])
9572  horizontalDistance([point1, point2]) == 10mm
9573}
9574";
9575
9576        let program = Program::parse(initial_source).unwrap().0.unwrap();
9577
9578        let mut frontend = FrontendState::new();
9579
9580        let mock_ctx = ExecutorContext::new_mock(None).await;
9581        let version = Version(0);
9582
9583        frontend.program = program.clone();
9584        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9585        frontend.update_state_after_exec(outcome, true);
9586        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9587        let sketch_id = sketch_object.id;
9588        let sketch = expect_sketch(sketch_object);
9589        let point2_id = *sketch.segments.get(1).unwrap();
9590
9591        let (src_delta, scene_delta) = frontend
9592            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9593            .await
9594            .unwrap();
9595        assert_eq!(
9596            src_delta.text.as_str(),
9597            "\
9598sketch(on = XY) {
9599  point1 = point(at = [var 1, var 2])
9600}
9601"
9602        );
9603        assert_eq!(
9604            scene_delta.new_graph.objects.len(),
9605            3,
9606            "{:#?}",
9607            scene_delta.new_graph.objects
9608        );
9609
9610        mock_ctx.close().await;
9611    }
9612
9613    #[tokio::test(flavor = "multi_thread")]
9614    async fn test_delete_line_cascades_to_fixed_constraint() {
9615        let initial_source = "\
9616sketch(on = XY) {
9617  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9618  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9619  fixed([line1.start, [0, 0]])
9620}
9621";
9622
9623        let program = Program::parse(initial_source).unwrap().0.unwrap();
9624
9625        let mut frontend = FrontendState::new();
9626
9627        let mock_ctx = ExecutorContext::new_mock(None).await;
9628        let version = Version(0);
9629
9630        frontend.program = program.clone();
9631        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9632        frontend.update_state_after_exec(outcome, true);
9633        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9634        let sketch_id = sketch_object.id;
9635        let sketch = expect_sketch(sketch_object);
9636        let line1_id = *sketch.segments.get(2).unwrap();
9637
9638        let (src_delta, scene_delta) = frontend
9639            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9640            .await
9641            .unwrap();
9642        assert_eq!(
9643            src_delta.text.as_str(),
9644            "\
9645sketch(on = XY) {
9646  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9647}
9648"
9649        );
9650        assert_eq!(
9651            scene_delta.new_graph.objects.len(),
9652            5,
9653            "{:#?}",
9654            scene_delta.new_graph.objects
9655        );
9656
9657        mock_ctx.close().await;
9658    }
9659
9660    #[tokio::test(flavor = "multi_thread")]
9661    async fn test_delete_line_cascades_to_midpoint_constraint() {
9662        let initial_source = "\
9663sketch(on = XY) {
9664  point1 = point(at = [var 1, var 2])
9665  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9666  midpoint(line1, point = point1)
9667}
9668";
9669
9670        let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672        let mut frontend = FrontendState::new();
9673
9674        let mock_ctx = ExecutorContext::new_mock(None).await;
9675        let version = Version(0);
9676
9677        frontend.program = program.clone();
9678        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9679        frontend.update_state_after_exec(outcome, true);
9680        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9681        let sketch_id = sketch_object.id;
9682        let sketch = expect_sketch(sketch_object);
9683        let line1_id = *sketch.segments.get(3).unwrap();
9684
9685        let (src_delta, scene_delta) = frontend
9686            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9687            .await
9688            .unwrap();
9689        assert_eq!(
9690            src_delta.text.as_str(),
9691            "\
9692sketch(on = XY) {
9693  point1 = point(at = [var 1, var 2])
9694}
9695"
9696        );
9697        assert_eq!(
9698            scene_delta.new_graph.objects.len(),
9699            3,
9700            "{:#?}",
9701            scene_delta.new_graph.objects
9702        );
9703
9704        mock_ctx.close().await;
9705    }
9706
9707    #[tokio::test(flavor = "multi_thread")]
9708    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9709        let initial_source = "\
9710sketch(on = XY) {
9711  point1 = point(at = [var 1, var 2])
9712  point2 = point(at = [var 3, var 4])
9713  point3 = point(at = [var 5, var 6])
9714  coincident([point1, point2, point3])
9715}
9716";
9717
9718        let program = Program::parse(initial_source).unwrap().0.unwrap();
9719
9720        let mut frontend = FrontendState::new();
9721
9722        let mock_ctx = ExecutorContext::new_mock(None).await;
9723        let version = Version(0);
9724
9725        frontend.program = program.clone();
9726        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9727        frontend.update_state_after_exec(outcome, true);
9728        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9729        let sketch_id = sketch_object.id;
9730        let sketch = expect_sketch(sketch_object);
9731        let point3_id = *sketch.segments.get(2).unwrap();
9732
9733        let (src_delta, scene_delta) = frontend
9734            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9735            .await
9736            .unwrap();
9737        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9738        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9739        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9740        assert!(
9741            src_delta.text.contains("coincident([point1, point2])"),
9742            "{}",
9743            src_delta.text
9744        );
9745
9746        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9747        let sketch = expect_sketch(sketch_object);
9748        assert_eq!(sketch.segments.len(), 2);
9749        assert_eq!(sketch.constraints.len(), 1);
9750
9751        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9752        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9753            panic!("Expected constraint object");
9754        };
9755        let Constraint::Coincident(coincident) = constraint else {
9756            panic!("Expected coincident constraint");
9757        };
9758        assert_eq!(
9759            coincident.segments,
9760            sketch
9761                .segments
9762                .iter()
9763                .copied()
9764                .map(Into::into)
9765                .collect::<Vec<ConstraintSegment>>()
9766        );
9767
9768        mock_ctx.close().await;
9769    }
9770
9771    #[tokio::test(flavor = "multi_thread")]
9772    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9773        let initial_source = "\
9774sketch(on = XY) {
9775  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9776  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9777  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9778  equalLength([line1, line2, line3])
9779}
9780";
9781
9782        let program = Program::parse(initial_source).unwrap().0.unwrap();
9783
9784        let mut frontend = FrontendState::new();
9785
9786        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9787        let mock_ctx = ExecutorContext::new_mock(None).await;
9788        let version = Version(0);
9789
9790        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9791        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9792        let sketch_id = sketch_object.id;
9793        let sketch = expect_sketch(sketch_object);
9794        let line3_id = *sketch.segments.get(8).unwrap();
9795
9796        let (src_delta, scene_delta) = frontend
9797            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9798            .await
9799            .unwrap();
9800        assert_eq!(
9801            src_delta.text.as_str(),
9802            "\
9803sketch(on = XY) {
9804  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9805  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9806  equalLength([line1, line2])
9807}
9808"
9809        );
9810
9811        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9812        let sketch = expect_sketch(sketch_object);
9813        assert_eq!(sketch.constraints.len(), 1);
9814
9815        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9816        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9817            panic!("Expected constraint object");
9818        };
9819        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9820            panic!("Expected lines equal length constraint");
9821        };
9822        assert_eq!(lines_equal_length.lines.len(), 2);
9823
9824        ctx.close().await;
9825        mock_ctx.close().await;
9826    }
9827
9828    #[tokio::test(flavor = "multi_thread")]
9829    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9830        let initial_source = "\
9831sketch(on = XY) {
9832  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9833  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9834  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9835  horizontal([line1.end, line2.start, line3.start])
9836}
9837";
9838
9839        let program = Program::parse(initial_source).unwrap().0.unwrap();
9840
9841        let mut frontend = FrontendState::new();
9842
9843        let mock_ctx = ExecutorContext::new_mock(None).await;
9844        let version = Version(0);
9845
9846        frontend.program = program.clone();
9847        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9848        frontend.update_state_after_exec(outcome, true);
9849        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9850        let sketch_id = sketch_object.id;
9851        let sketch = expect_sketch(sketch_object);
9852        let line1_id = *sketch.segments.get(2).unwrap();
9853
9854        let (src_delta, scene_delta) = frontend
9855            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9856            .await
9857            .unwrap();
9858        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9859        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9860        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9861        assert!(
9862            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9863            "{}",
9864            src_delta.text
9865        );
9866
9867        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9868        let sketch = expect_sketch(sketch_object);
9869        assert_eq!(sketch.constraints.len(), 1);
9870
9871        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9872        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9873            panic!("Expected constraint object");
9874        };
9875        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9876            panic!("Expected horizontal points constraint");
9877        };
9878        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9879        assert_eq!(*points, remaining_points);
9880
9881        mock_ctx.close().await;
9882    }
9883
9884    #[tokio::test(flavor = "multi_thread")]
9885    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9886        let initial_source = "\
9887sketch(on = XY) {
9888  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9889  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9890  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9891  vertical([line1.end, line2.start, line3.start])
9892}
9893";
9894
9895        let program = Program::parse(initial_source).unwrap().0.unwrap();
9896
9897        let mut frontend = FrontendState::new();
9898
9899        let mock_ctx = ExecutorContext::new_mock(None).await;
9900        let version = Version(0);
9901
9902        frontend.program = program.clone();
9903        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9904        frontend.update_state_after_exec(outcome, true);
9905        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9906        let sketch_id = sketch_object.id;
9907        let sketch = expect_sketch(sketch_object);
9908        let line1_id = *sketch.segments.get(2).unwrap();
9909
9910        let (src_delta, scene_delta) = frontend
9911            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9912            .await
9913            .unwrap();
9914        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9915        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9916        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9917        assert!(
9918            src_delta.text.contains("vertical([line2.start, line3.start])"),
9919            "{}",
9920            src_delta.text
9921        );
9922
9923        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9924        let sketch = expect_sketch(sketch_object);
9925        assert_eq!(sketch.constraints.len(), 1);
9926
9927        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9928        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9929            panic!("Expected constraint object");
9930        };
9931        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9932            panic!("Expected vertical points constraint");
9933        };
9934        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9935        assert_eq!(*points, remaining_points);
9936
9937        mock_ctx.close().await;
9938    }
9939
9940    #[tokio::test(flavor = "multi_thread")]
9941    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9942        let initial_source = "\
9943sketch(on = XY) {
9944  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9945  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9946  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9947  coincident([line1.end, line2.start, line3.start])
9948}
9949";
9950
9951        let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953        let mut frontend = FrontendState::new();
9954
9955        let mock_ctx = ExecutorContext::new_mock(None).await;
9956        let version = Version(0);
9957
9958        frontend.program = program.clone();
9959        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9960        frontend.update_state_after_exec(outcome, true);
9961        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9962        let sketch_id = sketch_object.id;
9963        let sketch = expect_sketch(sketch_object);
9964        let line1_id = *sketch.segments.get(2).unwrap();
9965
9966        let (src_delta, scene_delta) = frontend
9967            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9968            .await
9969            .unwrap();
9970        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9971        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9972        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9973        assert!(
9974            src_delta.text.contains("coincident([line2.start, line3.start])"),
9975            "{}",
9976            src_delta.text
9977        );
9978
9979        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9980        let sketch = expect_sketch(sketch_object);
9981        assert_eq!(sketch.constraints.len(), 1);
9982
9983        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9984        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9985            panic!("Expected constraint object");
9986        };
9987        let Constraint::Coincident(coincident) = constraint else {
9988            panic!("Expected coincident constraint");
9989        };
9990        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9991        assert_eq!(coincident.segments, remaining_segments);
9992
9993        mock_ctx.close().await;
9994    }
9995
9996    #[tokio::test(flavor = "multi_thread")]
9997    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9998        let initial_source = "\
9999sketch(on = XY) {
10000  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10001  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10002  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10003  equalLength([line1, line2, line3])
10004}
10005";
10006
10007        let program = Program::parse(initial_source).unwrap().0.unwrap();
10008
10009        let mut frontend = FrontendState::new();
10010
10011        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10012        let mock_ctx = ExecutorContext::new_mock(None).await;
10013        let version = Version(0);
10014
10015        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10016        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10017        let sketch_id = sketch_object.id;
10018        let sketch = expect_sketch(sketch_object);
10019        let line2_id = *sketch.segments.get(5).unwrap();
10020        let line3_id = *sketch.segments.get(8).unwrap();
10021
10022        let (src_delta, scene_delta) = frontend
10023            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10024            .await
10025            .unwrap();
10026        assert_eq!(
10027            src_delta.text.as_str(),
10028            "\
10029sketch(on = XY) {
10030  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10031}
10032"
10033        );
10034
10035        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10036        let sketch = expect_sketch(sketch_object);
10037        assert!(sketch.constraints.is_empty());
10038
10039        ctx.close().await;
10040        mock_ctx.close().await;
10041    }
10042
10043    #[tokio::test(flavor = "multi_thread")]
10044    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10045        let initial_source = "\
10046sketch(on = XY) {
10047  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10048  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10049  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10050  parallel([line1, line2, line3])
10051}
10052";
10053
10054        let program = Program::parse(initial_source).unwrap().0.unwrap();
10055
10056        let mut frontend = FrontendState::new();
10057
10058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10059        let mock_ctx = ExecutorContext::new_mock(None).await;
10060        let version = Version(0);
10061
10062        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10064        let sketch_id = sketch_object.id;
10065        let sketch = expect_sketch(sketch_object);
10066        let line3_id = *sketch.segments.get(8).unwrap();
10067
10068        let (src_delta, scene_delta) = frontend
10069            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10070            .await
10071            .unwrap();
10072        assert_eq!(
10073            src_delta.text.as_str(),
10074            "\
10075sketch(on = XY) {
10076  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10077  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10078  parallel([line1, line2])
10079}
10080"
10081        );
10082
10083        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10084        let sketch = expect_sketch(sketch_object);
10085        assert_eq!(sketch.constraints.len(), 1);
10086
10087        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10088        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10089            panic!("Expected constraint object");
10090        };
10091        let Constraint::Parallel(parallel) = constraint else {
10092            panic!("Expected parallel constraint");
10093        };
10094        assert_eq!(parallel.lines.len(), 2);
10095
10096        ctx.close().await;
10097        mock_ctx.close().await;
10098    }
10099
10100    #[tokio::test(flavor = "multi_thread")]
10101    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10102        let initial_source = "\
10103sketch(on = XY) {
10104  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10105  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10106  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10107  parallel([line1, line2, line3])
10108}
10109";
10110
10111        let program = Program::parse(initial_source).unwrap().0.unwrap();
10112
10113        let mut frontend = FrontendState::new();
10114
10115        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10116        let mock_ctx = ExecutorContext::new_mock(None).await;
10117        let version = Version(0);
10118
10119        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10120        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10121        let sketch_id = sketch_object.id;
10122        let sketch = expect_sketch(sketch_object);
10123        let line2_id = *sketch.segments.get(5).unwrap();
10124        let line3_id = *sketch.segments.get(8).unwrap();
10125
10126        let (src_delta, scene_delta) = frontend
10127            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10128            .await
10129            .unwrap();
10130        assert_eq!(
10131            src_delta.text.as_str(),
10132            "\
10133sketch(on = XY) {
10134  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10135}
10136"
10137        );
10138
10139        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10140        let sketch = expect_sketch(sketch_object);
10141        assert!(sketch.constraints.is_empty());
10142
10143        ctx.close().await;
10144        mock_ctx.close().await;
10145    }
10146
10147    #[tokio::test(flavor = "multi_thread")]
10148    async fn test_delete_line_line_coincident_constraint() {
10149        let initial_source = "\
10150sketch(on = XY) {
10151  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10152  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10153  coincident([line1, line2])
10154}
10155";
10156
10157        let program = Program::parse(initial_source).unwrap().0.unwrap();
10158
10159        let mut frontend = FrontendState::new();
10160
10161        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10162        let mock_ctx = ExecutorContext::new_mock(None).await;
10163        let version = Version(0);
10164
10165        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10166        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10167        let sketch_id = sketch_object.id;
10168        let sketch = expect_sketch(sketch_object);
10169
10170        let coincident_id = *sketch.constraints.first().unwrap();
10171
10172        let (src_delta, scene_delta) = frontend
10173            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10174            .await
10175            .unwrap();
10176        assert_eq!(
10177            src_delta.text.as_str(),
10178            "\
10179sketch(on = XY) {
10180  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10181  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10182}
10183"
10184        );
10185        assert_eq!(scene_delta.new_objects, vec![]);
10186        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10187
10188        ctx.close().await;
10189        mock_ctx.close().await;
10190    }
10191
10192    #[tokio::test(flavor = "multi_thread")]
10193    async fn test_two_points_coincident() {
10194        let initial_source = "\
10195sketch(on = XY) {
10196  point1 = point(at = [var 1, var 2])
10197  point(at = [3, 4])
10198}
10199";
10200
10201        let program = Program::parse(initial_source).unwrap().0.unwrap();
10202
10203        let mut frontend = FrontendState::new();
10204
10205        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10206        let mock_ctx = ExecutorContext::new_mock(None).await;
10207        let version = Version(0);
10208
10209        frontend.hack_set_program(&ctx, program).await.unwrap();
10210        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10211        let sketch_id = sketch_object.id;
10212        let sketch = expect_sketch(sketch_object);
10213        let point0_id = *sketch.segments.first().unwrap();
10214        let point1_id = *sketch.segments.get(1).unwrap();
10215
10216        let constraint = Constraint::Coincident(Coincident {
10217            segments: vec![point0_id.into(), point1_id.into()],
10218        });
10219        let (src_delta, scene_delta) = frontend
10220            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10221            .await
10222            .unwrap();
10223        assert_eq!(
10224            src_delta.text.as_str(),
10225            "\
10226sketch(on = XY) {
10227  point1 = point(at = [var 3, var 4])
10228  point2 = point(at = [3, 4])
10229  coincident([point1, point2])
10230}
10231"
10232        );
10233        assert_eq!(
10234            scene_delta.new_graph.objects.len(),
10235            5,
10236            "{:#?}",
10237            scene_delta.new_graph.objects
10238        );
10239
10240        ctx.close().await;
10241        mock_ctx.close().await;
10242    }
10243
10244    #[tokio::test(flavor = "multi_thread")]
10245    async fn test_three_points_coincident() {
10246        let initial_source = "\
10247sketch(on = XY) {
10248  point1 = point(at = [var 1, var 2])
10249  point(at = [var 3, var 4])
10250  point(at = [var 5, var 6])
10251}
10252";
10253
10254        let program = Program::parse(initial_source).unwrap().0.unwrap();
10255
10256        let mut frontend = FrontendState::new();
10257
10258        let mock_ctx = ExecutorContext::new_mock(None).await;
10259        let version = Version(0);
10260
10261        frontend.program = program.clone();
10262        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10263        frontend.update_state_after_exec(outcome, true);
10264        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10265        let sketch_id = sketch_object.id;
10266        let sketch = expect_sketch(sketch_object);
10267        let segments = sketch
10268            .segments
10269            .iter()
10270            .take(3)
10271            .copied()
10272            .map(Into::into)
10273            .collect::<Vec<ConstraintSegment>>();
10274
10275        let constraint = Constraint::Coincident(Coincident {
10276            segments: segments.clone(),
10277        });
10278        let (src_delta, scene_delta) = frontend
10279            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10280            .await
10281            .unwrap();
10282        assert_eq!(
10283            src_delta.text.as_str(),
10284            "\
10285sketch(on = XY) {
10286  point1 = point(at = [var 3, var 4])
10287  point2 = point(at = [var 3, var 4])
10288  point3 = point(at = [var 3, var 4])
10289  coincident([point1, point2, point3])
10290}
10291"
10292        );
10293
10294        let constraint_object = scene_delta
10295            .new_graph
10296            .objects
10297            .iter()
10298            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10299            .unwrap();
10300
10301        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10302            panic!("expected a constraint object");
10303        };
10304
10305        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10306
10307        mock_ctx.close().await;
10308    }
10309
10310    #[tokio::test(flavor = "multi_thread")]
10311    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10312        let initial_source = "\
10313sketch(on = XY) {
10314  point1 = point(at = [var 1, var 2])
10315  point2 = point(at = [var 3, var 4])
10316  point3 = point(at = [var 5, var 6])
10317  coincident([point1, point2, point3])
10318}
10319";
10320
10321        let program = Program::parse(initial_source).unwrap().0.unwrap();
10322
10323        let mut frontend = FrontendState::new();
10324
10325        let ctx = ExecutorContext::new_mock(None).await;
10326        frontend.program = program.clone();
10327        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10328        frontend.update_state_after_exec(outcome, true);
10329
10330        let constraint_object = frontend
10331            .scene_graph
10332            .objects
10333            .iter()
10334            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10335            .unwrap();
10336        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10337            panic!("expected a constraint object");
10338        };
10339
10340        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10341        let sketch = expect_sketch(sketch_object);
10342        let expected_segments = sketch
10343            .segments
10344            .iter()
10345            .take(3)
10346            .copied()
10347            .map(Into::into)
10348            .collect::<Vec<ConstraintSegment>>();
10349
10350        assert_eq!(
10351            constraint,
10352            &Constraint::Coincident(Coincident {
10353                segments: expected_segments,
10354            })
10355        );
10356
10357        ctx.close().await;
10358    }
10359
10360    #[tokio::test(flavor = "multi_thread")]
10361    async fn test_point_origin_coincident_preserves_order() {
10362        let initial_source = "\
10363sketch(on = XY) {
10364  point(at = [var 1, var 2])
10365}
10366";
10367
10368        for (origin_first, expected_source) in [
10369            (
10370                true,
10371                "\
10372sketch(on = XY) {
10373  point1 = point(at = [var 0, var 0])
10374  coincident([ORIGIN, point1])
10375}
10376",
10377            ),
10378            (
10379                false,
10380                "\
10381sketch(on = XY) {
10382  point1 = point(at = [var 0, var 0])
10383  coincident([point1, ORIGIN])
10384}
10385",
10386            ),
10387        ] {
10388            let program = Program::parse(initial_source).unwrap().0.unwrap();
10389
10390            let mut frontend = FrontendState::new();
10391
10392            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10393            let mock_ctx = ExecutorContext::new_mock(None).await;
10394            let version = Version(0);
10395
10396            frontend.hack_set_program(&ctx, program).await.unwrap();
10397            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10398            let sketch_id = sketch_object.id;
10399            let sketch = expect_sketch(sketch_object);
10400            let point_id = *sketch.segments.first().unwrap();
10401
10402            let segments = if origin_first {
10403                vec![ConstraintSegment::ORIGIN, point_id.into()]
10404            } else {
10405                vec![point_id.into(), ConstraintSegment::ORIGIN]
10406            };
10407            let constraint = Constraint::Coincident(Coincident {
10408                segments: segments.clone(),
10409            });
10410            let (src_delta, scene_delta) = frontend
10411                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10412                .await
10413                .unwrap();
10414            assert_eq!(src_delta.text.as_str(), expected_source);
10415
10416            let constraint_object = scene_delta
10417                .new_graph
10418                .objects
10419                .iter()
10420                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10421                .unwrap();
10422
10423            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10424                panic!("expected a constraint object");
10425            };
10426
10427            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10428
10429            ctx.close().await;
10430            mock_ctx.close().await;
10431        }
10432    }
10433
10434    #[tokio::test(flavor = "multi_thread")]
10435    async fn test_coincident_of_line_end_points() {
10436        let initial_source = "\
10437sketch(on = XY) {
10438  line(start = [var 1, var 2], end = [var 3, var 4])
10439  line(start = [var 5, var 6], end = [var 7, var 8])
10440}
10441";
10442
10443        let program = Program::parse(initial_source).unwrap().0.unwrap();
10444
10445        let mut frontend = FrontendState::new();
10446
10447        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10448        let mock_ctx = ExecutorContext::new_mock(None).await;
10449        let version = Version(0);
10450
10451        frontend.hack_set_program(&ctx, program).await.unwrap();
10452        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10453        let sketch_id = sketch_object.id;
10454        let sketch = expect_sketch(sketch_object);
10455        let point0_id = *sketch.segments.get(1).unwrap();
10456        let point1_id = *sketch.segments.get(3).unwrap();
10457
10458        let constraint = Constraint::Coincident(Coincident {
10459            segments: vec![point0_id.into(), point1_id.into()],
10460        });
10461        let (src_delta, scene_delta) = frontend
10462            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10463            .await
10464            .unwrap();
10465        assert_eq!(
10466            src_delta.text.as_str(),
10467            "\
10468sketch(on = XY) {
10469  line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10470  line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10471  coincident([line1.end, line2.start])
10472}
10473"
10474        );
10475        assert_eq!(
10476            scene_delta.new_graph.objects.len(),
10477            9,
10478            "{:#?}",
10479            scene_delta.new_graph.objects
10480        );
10481
10482        ctx.close().await;
10483        mock_ctx.close().await;
10484    }
10485
10486    #[tokio::test(flavor = "multi_thread")]
10487    async fn test_coincident_of_line_point_and_circle_segment() {
10488        let initial_source = "\
10489sketch(on = XY) {
10490  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10491  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10492}
10493";
10494        let program = Program::parse(initial_source).unwrap().0.unwrap();
10495        let mut frontend = FrontendState::new();
10496
10497        let mock_ctx = ExecutorContext::new_mock(None).await;
10498        let version = Version(0);
10499
10500        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10501        frontend.program = program;
10502        frontend.update_state_after_exec(outcome, true);
10503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10504        let sketch_id = sketch_object.id;
10505        let sketch = expect_sketch(sketch_object);
10506
10507        let circle_id = sketch
10508            .segments
10509            .iter()
10510            .copied()
10511            .find(|seg_id| {
10512                matches!(
10513                    &frontend.scene_graph.objects[seg_id.0].kind,
10514                    ObjectKind::Segment {
10515                        segment: Segment::Circle(_)
10516                    }
10517                )
10518            })
10519            .expect("Expected a circle segment in sketch");
10520        let line_id = frontend
10521            .scene_graph
10522            .objects
10523            .iter()
10524            .find_map(|obj| match &obj.kind {
10525                ObjectKind::Segment {
10526                    segment: Segment::Line(line),
10527                } if line.owner.is_none() => Some(obj.id),
10528                _ => None,
10529            })
10530            .expect("Expected a standalone line segment in scene graph");
10531
10532        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10533            ObjectKind::Segment {
10534                segment: Segment::Line(line),
10535            } => line.start,
10536            _ => panic!("Expected line segment object"),
10537        };
10538
10539        let constraint = Constraint::Coincident(Coincident {
10540            segments: vec![line_start_point_id.into(), circle_id.into()],
10541        });
10542        let (src_delta, _scene_delta) = frontend
10543            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10544            .await
10545            .unwrap();
10546        assert_eq!(
10547            src_delta.text.as_str(),
10548            "\
10549sketch(on = XY) {
10550  circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10551  line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10552  coincident([line1.start, circle1])
10553}
10554"
10555        );
10556
10557        mock_ctx.close().await;
10558    }
10559
10560    #[tokio::test(flavor = "multi_thread")]
10561    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10562        // Test that attempting an invalid coincident constraint (arc and line)
10563        // doesn't corrupt the state, allowing subsequent operations to work.
10564        // This test verifies the transactional fix in add_constraint that prevents
10565        // state corruption when invalid constraints are attempted.
10566        // Example: coincident constraint between an arc segment and a straight line segment
10567        // is geometrically invalid and should fail, but state should remain intact.
10568        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10569        let program = Program::empty();
10570
10571        let mut frontend = FrontendState::new();
10572        frontend.program = program;
10573
10574        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10575        let mock_ctx = ExecutorContext::new_mock(None).await;
10576        let version = Version(0);
10577
10578        let sketch_args = SketchCtor {
10579            on: Plane::Default(PlaneName::Xy),
10580        };
10581        let (_src_delta, _scene_delta, sketch_id) = frontend
10582            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10583            .await
10584            .unwrap();
10585
10586        // Add an arc segment
10587        let arc_ctor = ArcCtor {
10588            start: Point2d {
10589                x: Expr::Var(Number {
10590                    value: 0.0,
10591                    units: NumericSuffix::Mm,
10592                }),
10593                y: Expr::Var(Number {
10594                    value: 0.0,
10595                    units: NumericSuffix::Mm,
10596                }),
10597            },
10598            end: Point2d {
10599                x: Expr::Var(Number {
10600                    value: 10.0,
10601                    units: NumericSuffix::Mm,
10602                }),
10603                y: Expr::Var(Number {
10604                    value: 10.0,
10605                    units: NumericSuffix::Mm,
10606                }),
10607            },
10608            center: Point2d {
10609                x: Expr::Var(Number {
10610                    value: 10.0,
10611                    units: NumericSuffix::Mm,
10612                }),
10613                y: Expr::Var(Number {
10614                    value: 0.0,
10615                    units: NumericSuffix::Mm,
10616                }),
10617            },
10618            construction: None,
10619        };
10620        let (_src_delta, scene_delta) = frontend
10621            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10622            .await
10623            .unwrap();
10624        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10625        let arc_id = *scene_delta.new_objects.last().unwrap();
10626
10627        // Add a line segment
10628        let line_ctor = LineCtor {
10629            start: Point2d {
10630                x: Expr::Var(Number {
10631                    value: 20.0,
10632                    units: NumericSuffix::Mm,
10633                }),
10634                y: Expr::Var(Number {
10635                    value: 0.0,
10636                    units: NumericSuffix::Mm,
10637                }),
10638            },
10639            end: Point2d {
10640                x: Expr::Var(Number {
10641                    value: 30.0,
10642                    units: NumericSuffix::Mm,
10643                }),
10644                y: Expr::Var(Number {
10645                    value: 10.0,
10646                    units: NumericSuffix::Mm,
10647                }),
10648            },
10649            construction: None,
10650        };
10651        let (_src_delta, scene_delta) = frontend
10652            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10653            .await
10654            .unwrap();
10655        // The line is the last object in new_objects (after the 2 points: start, end)
10656        let line_id = *scene_delta.new_objects.last().unwrap();
10657
10658        // Attempt to add an invalid coincident constraint between arc and line
10659        // This should fail during execution, but state should remain intact
10660        let constraint = Constraint::Coincident(Coincident {
10661            segments: vec![arc_id.into(), line_id.into()],
10662        });
10663        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10664
10665        // The constraint addition should fail (invalid constraint)
10666        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10667
10668        // Verify state is not corrupted by checking that we can still access the scene graph
10669        // and that the original segments are still present with their source ranges
10670        let sketch_object_after =
10671            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10672        let sketch_after = expect_sketch(sketch_object_after);
10673
10674        // Verify both segments are still in the sketch
10675        assert!(
10676            sketch_after.segments.contains(&arc_id),
10677            "Arc segment should still exist after failed constraint"
10678        );
10679        assert!(
10680            sketch_after.segments.contains(&line_id),
10681            "Line segment should still exist after failed constraint"
10682        );
10683
10684        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10685        let arc_obj = frontend
10686            .scene_graph
10687            .objects
10688            .get(arc_id.0)
10689            .expect("Arc object should still be accessible");
10690        let line_obj = frontend
10691            .scene_graph
10692            .objects
10693            .get(line_id.0)
10694            .expect("Line object should still be accessible");
10695
10696        // Verify source ranges are still valid (not corrupted)
10697        // Just verify that the objects are still accessible and have the expected types
10698        match &arc_obj.kind {
10699            ObjectKind::Segment {
10700                segment: Segment::Arc(_),
10701            } => {}
10702            _ => panic!("Arc object should still be an arc segment"),
10703        }
10704        match &line_obj.kind {
10705            ObjectKind::Segment {
10706                segment: Segment::Line(_),
10707            } => {}
10708            _ => panic!("Line object should still be a line segment"),
10709        }
10710
10711        ctx.close().await;
10712        mock_ctx.close().await;
10713    }
10714
10715    #[tokio::test(flavor = "multi_thread")]
10716    async fn test_distance_two_points() {
10717        let initial_source = "\
10718sketch(on = XY) {
10719  point(at = [var 1, var 2])
10720  point(at = [var 3, var 4])
10721}
10722";
10723
10724        let program = Program::parse(initial_source).unwrap().0.unwrap();
10725
10726        let mut frontend = FrontendState::new();
10727
10728        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10729        let mock_ctx = ExecutorContext::new_mock(None).await;
10730        let version = Version(0);
10731
10732        frontend.hack_set_program(&ctx, program).await.unwrap();
10733        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10734        let sketch_id = sketch_object.id;
10735        let sketch = expect_sketch(sketch_object);
10736        let point0_id = *sketch.segments.first().unwrap();
10737        let point1_id = *sketch.segments.get(1).unwrap();
10738
10739        let constraint = Constraint::Distance(Distance {
10740            points: vec![point0_id.into(), point1_id.into()],
10741            distance: Number {
10742                value: 2.0,
10743                units: NumericSuffix::Mm,
10744            },
10745            label_position: None,
10746            source: Default::default(),
10747        });
10748        let (src_delta, scene_delta) = frontend
10749            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10750            .await
10751            .unwrap();
10752        assert_eq!(
10753            src_delta.text.as_str(),
10754            // The lack indentation is a formatter bug.
10755            "\
10756sketch(on = XY) {
10757  point1 = point(at = [var 1.29, var 2.29])
10758  point2 = point(at = [var 2.71, var 3.71])
10759  distance([point1, point2]) == 2mm
10760}
10761"
10762        );
10763        assert_eq!(
10764            scene_delta.new_graph.objects.len(),
10765            5,
10766            "{:#?}",
10767            scene_delta.new_graph.objects
10768        );
10769
10770        ctx.close().await;
10771        mock_ctx.close().await;
10772    }
10773
10774    #[tokio::test(flavor = "multi_thread")]
10775    async fn test_distance_two_points_with_label() {
10776        let initial_source = "\
10777sketch(on = XY) {
10778  point(at = [var 1, var 2])
10779  point(at = [var 3, var 4])
10780}
10781";
10782
10783        let program = Program::parse(initial_source).unwrap().0.unwrap();
10784
10785        let mut frontend = FrontendState::new();
10786
10787        let mock_ctx = ExecutorContext::new_mock(None).await;
10788        let version = Version(0);
10789
10790        frontend.program = program.clone();
10791        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10792        frontend.update_state_after_exec(outcome, true);
10793        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10794        let sketch_id = sketch_object.id;
10795        let sketch = expect_sketch(sketch_object);
10796        let point0_id = *sketch.segments.first().unwrap();
10797        let point1_id = *sketch.segments.get(1).unwrap();
10798
10799        let label_position = Point2d {
10800            x: Number {
10801                value: 10.0,
10802                units: NumericSuffix::Mm,
10803            },
10804            y: Number {
10805                value: 11.0,
10806                units: NumericSuffix::Mm,
10807            },
10808        };
10809        let constraint = Constraint::Distance(Distance {
10810            points: vec![point0_id.into(), point1_id.into()],
10811            distance: Number {
10812                value: 2.0,
10813                units: NumericSuffix::Mm,
10814            },
10815            label_position: Some(label_position.clone()),
10816            source: Default::default(),
10817        });
10818        let (src_delta, scene_delta) = frontend
10819            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10820            .await
10821            .unwrap();
10822        assert_eq!(
10823            src_delta.text.as_str(),
10824            "\
10825sketch(on = XY) {
10826  point1 = point(at = [var 1.29, var 2.29])
10827  point2 = point(at = [var 2.71, var 3.71])
10828  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10829}
10830"
10831        );
10832
10833        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10834        let sketch = expect_sketch(sketch_object);
10835        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10836        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10837            panic!("Expected constraint object");
10838        };
10839        let Constraint::Distance(distance) = constraint else {
10840            panic!("Expected distance constraint");
10841        };
10842        assert_eq!(distance.label_position, Some(label_position));
10843
10844        mock_ctx.close().await;
10845    }
10846
10847    #[tokio::test(flavor = "multi_thread")]
10848    async fn test_edit_distance_constraint_label_position() {
10849        let initial_source = "\
10850sketch(on = XY) {
10851  point(at = [var 1, var 2])
10852  point(at = [var 3, var 2])
10853}
10854";
10855
10856        let program = Program::parse(initial_source).unwrap().0.unwrap();
10857
10858        let mut frontend = FrontendState::new();
10859
10860        let mock_ctx = ExecutorContext::new_mock(None).await;
10861        let version = Version(0);
10862
10863        frontend.program = program.clone();
10864        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10865        frontend.update_state_after_exec(outcome, true);
10866        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10867        let sketch_id = sketch_object.id;
10868        let sketch = expect_sketch(sketch_object);
10869        let point0_id = *sketch.segments.first().unwrap();
10870        let point1_id = *sketch.segments.get(1).unwrap();
10871
10872        let constraint = Constraint::Distance(Distance {
10873            points: vec![point0_id.into(), point1_id.into()],
10874            distance: Number {
10875                value: 2.0,
10876                units: NumericSuffix::Mm,
10877            },
10878            label_position: None,
10879            source: Default::default(),
10880        });
10881        let (_, scene_delta) = frontend
10882            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10883            .await
10884            .unwrap();
10885        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10886        let sketch = expect_sketch(sketch_object);
10887        let constraint_id = sketch.constraints[0];
10888        let label_position = Point2d {
10889            x: Number {
10890                value: 10.0,
10891                units: NumericSuffix::Mm,
10892            },
10893            y: Number {
10894                value: 11.0,
10895                units: NumericSuffix::Mm,
10896            },
10897        };
10898
10899        let (src_delta, scene_delta) = frontend
10900            .edit_distance_constraint_label_position(
10901                &mock_ctx,
10902                version,
10903                sketch_id,
10904                constraint_id,
10905                label_position.clone(),
10906                vec![],
10907            )
10908            .await
10909            .unwrap();
10910        assert_eq!(
10911            src_delta.text.as_str(),
10912            "\
10913sketch(on = XY) {
10914  point1 = point(at = [var 1, var 2])
10915  point2 = point(at = [var 3, var 2])
10916  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10917}
10918"
10919        );
10920
10921        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10922        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10923            panic!("Expected constraint object");
10924        };
10925        let Constraint::Distance(distance) = constraint else {
10926            panic!("Expected distance constraint");
10927        };
10928        assert_eq!(distance.label_position, Some(label_position));
10929
10930        mock_ctx.close().await;
10931    }
10932
10933    #[tokio::test(flavor = "multi_thread")]
10934    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10935        let initial_source = "\
10936sketch(on = XY) {
10937  point1 = point(at = [var 0mm, var 0mm])
10938  point2 = point(at = [var 10mm, var 0mm])
10939  distance([point1, point2]) == 5mm
10940}
10941";
10942
10943        let program = Program::parse(initial_source).unwrap().0.unwrap();
10944        let mut frontend = FrontendState::new();
10945        let mock_ctx = ExecutorContext::new_mock(None).await;
10946        let version = Version(0);
10947
10948        frontend.program = program.clone();
10949        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10950        frontend.update_state_after_exec(outcome, true);
10951        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10952        let sketch_id = sketch_object.id;
10953        let sketch = expect_sketch(sketch_object);
10954        let point0_id = sketch.segments[0];
10955        let point1_id = sketch.segments[1];
10956        let constraint_id = sketch.constraints[0];
10957
10958        let edited_segments = vec![ExistingSegmentCtor {
10959            id: point0_id,
10960            ctor: SegmentCtor::Point(PointCtor {
10961                position: Point2d {
10962                    x: Expr::Var(Number {
10963                        value: 2.0,
10964                        units: NumericSuffix::Mm,
10965                    }),
10966                    y: Expr::Var(Number {
10967                        value: 1.0,
10968                        units: NumericSuffix::Mm,
10969                    }),
10970                },
10971            }),
10972        }];
10973        let (_, scene_delta) = frontend
10974            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10975            .await
10976            .unwrap();
10977        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10978        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10979
10980        let label_position = Point2d {
10981            x: Number {
10982                value: 3.0,
10983                units: NumericSuffix::Mm,
10984            },
10985            y: Number {
10986                value: 4.0,
10987                units: NumericSuffix::Mm,
10988            },
10989        };
10990        let (_, scene_delta) = frontend
10991            .edit_distance_constraint_label_position(
10992                &mock_ctx,
10993                version,
10994                sketch_id,
10995                constraint_id,
10996                label_position,
10997                vec![point0_id],
10998            )
10999            .await
11000            .unwrap();
11001
11002        assert_point_position_close(
11003            point_position(&scene_delta.new_graph, point0_id),
11004            point0_after_segment_edit,
11005        );
11006        assert_point_position_close(
11007            point_position(&scene_delta.new_graph, point1_id),
11008            point1_after_segment_edit,
11009        );
11010
11011        mock_ctx.close().await;
11012    }
11013
11014    #[tokio::test(flavor = "multi_thread")]
11015    async fn test_distance_point_line() {
11016        let initial_source = "\
11017sketch(on = XY) {
11018  point(at = [var 0, var 5])
11019  line(start = [var 0, var 0], end = [var 10, var 0])
11020}
11021";
11022
11023        let program = Program::parse(initial_source).unwrap().0.unwrap();
11024
11025        let mut frontend = FrontendState::new();
11026
11027        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11028        let mock_ctx = ExecutorContext::new_mock(None).await;
11029        let version = Version(0);
11030
11031        frontend.hack_set_program(&ctx, program).await.unwrap();
11032        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11033        let sketch_id = sketch_object.id;
11034        let sketch = expect_sketch(sketch_object);
11035        let point_id = *sketch.segments.first().unwrap();
11036        let line_id = *sketch
11037            .segments
11038            .iter()
11039            .find(|segment_id| {
11040                matches!(
11041                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11042                    Some(ObjectKind::Segment {
11043                        segment: Segment::Line(_)
11044                    })
11045                )
11046            })
11047            .unwrap();
11048
11049        let label_position = Point2d {
11050            x: Number {
11051                value: 10.0,
11052                units: NumericSuffix::Mm,
11053            },
11054            y: Number {
11055                value: 11.0,
11056                units: NumericSuffix::Mm,
11057            },
11058        };
11059        let constraint = Constraint::Distance(Distance {
11060            points: vec![point_id.into(), line_id.into()],
11061            distance: Number {
11062                value: 5.0,
11063                units: NumericSuffix::Mm,
11064            },
11065            label_position: Some(label_position.clone()),
11066            source: Default::default(),
11067        });
11068        let (src_delta, scene_delta) = frontend
11069            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11070            .await
11071            .unwrap();
11072        assert_eq!(
11073            src_delta.text.as_str(),
11074            "\
11075sketch(on = XY) {
11076  point1 = point(at = [var 0, var 5])
11077  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11078  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11079}
11080"
11081        );
11082        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11083        let sketch = expect_sketch(sketch_object);
11084        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11085        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11086            panic!("Expected constraint object");
11087        };
11088        let Constraint::Distance(distance) = constraint else {
11089            panic!("Expected distance constraint");
11090        };
11091        assert_eq!(distance.label_position, Some(label_position));
11092
11093        ctx.close().await;
11094        mock_ctx.close().await;
11095    }
11096
11097    #[tokio::test(flavor = "multi_thread")]
11098    async fn test_distance_point_arc() {
11099        let initial_source = "\
11100sketch(on = XY) {
11101  point(at = [var 0, var 8])
11102  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11103}
11104";
11105
11106        let program = Program::parse(initial_source).unwrap().0.unwrap();
11107
11108        let mut frontend = FrontendState::new();
11109
11110        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11111        let mock_ctx = ExecutorContext::new_mock(None).await;
11112        let version = Version(0);
11113
11114        frontend.hack_set_program(&ctx, program).await.unwrap();
11115        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11116        let sketch_id = sketch_object.id;
11117        let sketch = expect_sketch(sketch_object);
11118        let point_id = *sketch.segments.first().unwrap();
11119        let arc_id = *sketch
11120            .segments
11121            .iter()
11122            .find(|segment_id| {
11123                matches!(
11124                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11125                    Some(ObjectKind::Segment {
11126                        segment: Segment::Arc(_)
11127                    })
11128                )
11129            })
11130            .unwrap();
11131
11132        let constraint = Constraint::Distance(Distance {
11133            points: vec![point_id.into(), arc_id.into()],
11134            distance: Number {
11135                value: 3.0,
11136                units: NumericSuffix::Mm,
11137            },
11138            label_position: None,
11139            source: Default::default(),
11140        });
11141        let (src_delta, _scene_delta) = frontend
11142            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11143            .await
11144            .unwrap();
11145        assert_eq!(
11146            src_delta.text.as_str(),
11147            "\
11148sketch(on = XY) {
11149  point1 = point(at = [var 0, var 8])
11150  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11151  distance([point1, arc1]) == 3mm
11152}
11153"
11154        );
11155
11156        ctx.close().await;
11157        mock_ctx.close().await;
11158    }
11159
11160    #[tokio::test(flavor = "multi_thread")]
11161    async fn test_distance_arc_origin() {
11162        let initial_source = "\
11163sketch001 = sketch(on = XY) {
11164  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11165}
11166";
11167
11168        let program = Program::parse(initial_source).unwrap().0.unwrap();
11169
11170        let mut frontend = FrontendState::new();
11171
11172        let mock_ctx = ExecutorContext::new_mock(None).await;
11173        let version = Version(0);
11174
11175        frontend.program = program.clone();
11176        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11177        frontend.update_state_after_exec(outcome, true);
11178        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11179        let sketch_id = sketch_object.id;
11180        let sketch = expect_sketch(sketch_object);
11181        let arc_id = *sketch
11182            .segments
11183            .iter()
11184            .find(|segment_id| {
11185                matches!(
11186                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11187                    Some(ObjectKind::Segment {
11188                        segment: Segment::Arc(_)
11189                    })
11190                )
11191            })
11192            .unwrap();
11193
11194        let constraint = Constraint::Distance(Distance {
11195            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11196            distance: Number {
11197                value: 3.0,
11198                units: NumericSuffix::Mm,
11199            },
11200            label_position: None,
11201            source: Default::default(),
11202        });
11203        let (src_delta, _scene_delta) = frontend
11204            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11205            .await
11206            .unwrap();
11207        assert_eq!(
11208            src_delta.text.as_str(),
11209            "\
11210sketch001 = sketch(on = XY) {
11211  arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11212  distance([arc1, ORIGIN]) == 3mm
11213}
11214"
11215        );
11216
11217        mock_ctx.close().await;
11218    }
11219
11220    #[tokio::test(flavor = "multi_thread")]
11221    async fn test_distance_line_origin() {
11222        let initial_source = "\
11223sketch(on = XY) {
11224  line(start = [var 5, var 0], end = [var 5, var 10])
11225}
11226";
11227
11228        let program = Program::parse(initial_source).unwrap().0.unwrap();
11229
11230        let mut frontend = FrontendState::new();
11231
11232        let mock_ctx = ExecutorContext::new_mock(None).await;
11233        let version = Version(0);
11234
11235        frontend.program = program.clone();
11236        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11237        frontend.update_state_after_exec(outcome, true);
11238        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11239        let sketch_id = sketch_object.id;
11240        let sketch = expect_sketch(sketch_object);
11241        let line_id = *sketch
11242            .segments
11243            .iter()
11244            .find(|segment_id| {
11245                matches!(
11246                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11247                    Some(ObjectKind::Segment {
11248                        segment: Segment::Line(_)
11249                    })
11250                )
11251            })
11252            .unwrap();
11253
11254        let constraint = Constraint::Distance(Distance {
11255            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11256            distance: Number {
11257                value: 5.0,
11258                units: NumericSuffix::Mm,
11259            },
11260            label_position: None,
11261            source: Default::default(),
11262        });
11263        let (src_delta, _scene_delta) = frontend
11264            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11265            .await
11266            .unwrap();
11267        assert_eq!(
11268            src_delta.text.as_str(),
11269            "\
11270sketch(on = XY) {
11271  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11272  distance([ORIGIN, line1]) == 5mm
11273}
11274"
11275        );
11276
11277        mock_ctx.close().await;
11278    }
11279
11280    #[tokio::test(flavor = "multi_thread")]
11281    async fn test_distance_line_circle() {
11282        let initial_source = "\
11283sketch(on = XY) {
11284  line(start = [var -10, var 8], end = [var 10, var 8])
11285  circle(start = [var 5, var 0], center = [var 0, var 0])
11286}
11287";
11288
11289        let program = Program::parse(initial_source).unwrap().0.unwrap();
11290
11291        let mut frontend = FrontendState::new();
11292
11293        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11294        let mock_ctx = ExecutorContext::new_mock(None).await;
11295        let version = Version(0);
11296
11297        frontend.hack_set_program(&ctx, program).await.unwrap();
11298        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11299        let sketch_id = sketch_object.id;
11300        let sketch = expect_sketch(sketch_object);
11301        let line_id = *sketch
11302            .segments
11303            .iter()
11304            .find(|segment_id| {
11305                matches!(
11306                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11307                    Some(ObjectKind::Segment {
11308                        segment: Segment::Line(_)
11309                    })
11310                )
11311            })
11312            .unwrap();
11313        let circle_id = *sketch
11314            .segments
11315            .iter()
11316            .find(|segment_id| {
11317                matches!(
11318                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11319                    Some(ObjectKind::Segment {
11320                        segment: Segment::Circle(_)
11321                    })
11322                )
11323            })
11324            .unwrap();
11325
11326        let constraint = Constraint::Distance(Distance {
11327            points: vec![line_id.into(), circle_id.into()],
11328            distance: Number {
11329                value: 3.0,
11330                units: NumericSuffix::Mm,
11331            },
11332            label_position: None,
11333            source: Default::default(),
11334        });
11335        let (src_delta, _scene_delta) = frontend
11336            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11337            .await
11338            .unwrap();
11339        assert_eq!(
11340            src_delta.text.as_str(),
11341            "\
11342sketch(on = XY) {
11343  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11344  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11345  distance([line1, circle1]) == 3mm
11346}
11347"
11348        );
11349
11350        ctx.close().await;
11351        mock_ctx.close().await;
11352    }
11353
11354    #[tokio::test(flavor = "multi_thread")]
11355    async fn test_distance_circle_arc() {
11356        let initial_source = "\
11357sketch(on = XY) {
11358  circle(start = [var 5, var 0], center = [var 0, var 0])
11359  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11360}
11361";
11362
11363        let program = Program::parse(initial_source).unwrap().0.unwrap();
11364
11365        let mut frontend = FrontendState::new();
11366
11367        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11368        let mock_ctx = ExecutorContext::new_mock(None).await;
11369        let version = Version(0);
11370
11371        frontend.hack_set_program(&ctx, program).await.unwrap();
11372        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11373        let sketch_id = sketch_object.id;
11374        let sketch = expect_sketch(sketch_object);
11375        let circle_id = *sketch
11376            .segments
11377            .iter()
11378            .find(|segment_id| {
11379                matches!(
11380                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11381                    Some(ObjectKind::Segment {
11382                        segment: Segment::Circle(_)
11383                    })
11384                )
11385            })
11386            .unwrap();
11387        let arc_id = *sketch
11388            .segments
11389            .iter()
11390            .find(|segment_id| {
11391                matches!(
11392                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11393                    Some(ObjectKind::Segment {
11394                        segment: Segment::Arc(_)
11395                    })
11396                )
11397            })
11398            .unwrap();
11399
11400        let constraint = Constraint::Distance(Distance {
11401            points: vec![circle_id.into(), arc_id.into()],
11402            distance: Number {
11403                value: 3.0,
11404                units: NumericSuffix::Mm,
11405            },
11406            label_position: None,
11407            source: Default::default(),
11408        });
11409        let (src_delta, _scene_delta) = frontend
11410            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11411            .await
11412            .unwrap();
11413        assert_eq!(
11414            src_delta.text.as_str(),
11415            "\
11416sketch(on = XY) {
11417  circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11418  arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11419  distance([circle1, arc1]) == 3mm
11420}
11421"
11422        );
11423
11424        ctx.close().await;
11425        mock_ctx.close().await;
11426    }
11427
11428    #[tokio::test(flavor = "multi_thread")]
11429    async fn test_distance_parallel_lines() {
11430        let initial_source = "\
11431sketch(on = XY) {
11432  line(start = [var 0, var 0], end = [var 10, var 0])
11433  line(start = [var 0, var 5], end = [var 10, var 5])
11434}
11435";
11436
11437        let program = Program::parse(initial_source).unwrap().0.unwrap();
11438
11439        let mut frontend = FrontendState::new();
11440
11441        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11442        let mock_ctx = ExecutorContext::new_mock(None).await;
11443        let version = Version(0);
11444
11445        frontend.hack_set_program(&ctx, program).await.unwrap();
11446        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11447        let sketch_id = sketch_object.id;
11448        let sketch = expect_sketch(sketch_object);
11449        let line_ids = sketch
11450            .segments
11451            .iter()
11452            .copied()
11453            .filter(|segment_id| {
11454                matches!(
11455                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11456                    Some(ObjectKind::Segment {
11457                        segment: Segment::Line(_)
11458                    })
11459                )
11460            })
11461            .collect::<Vec<_>>();
11462
11463        let constraint = Constraint::Distance(Distance {
11464            points: vec![line_ids[0].into(), line_ids[1].into()],
11465            distance: Number {
11466                value: 5.0,
11467                units: NumericSuffix::Mm,
11468            },
11469            label_position: None,
11470            source: Default::default(),
11471        });
11472        let (src_delta, _scene_delta) = frontend
11473            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11474            .await
11475            .unwrap();
11476        assert_eq!(
11477            src_delta.text.as_str(),
11478            "\
11479sketch(on = XY) {
11480  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11481  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11482  distance([line1, line2]) == 5mm
11483}
11484"
11485        );
11486
11487        ctx.close().await;
11488        mock_ctx.close().await;
11489    }
11490
11491    #[tokio::test(flavor = "multi_thread")]
11492    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11493        let initial_source = "\
11494sketch(on = XY) {
11495  line(start = [var 0, var 0], end = [var 10, var 0])
11496  line(start = [var 0, var 0], end = [var 0, var 10])
11497}
11498";
11499
11500        let program = Program::parse(initial_source).unwrap().0.unwrap();
11501
11502        let mut frontend = FrontendState::new();
11503
11504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11505        let mock_ctx = ExecutorContext::new_mock(None).await;
11506        let version = Version(0);
11507
11508        frontend.hack_set_program(&ctx, program).await.unwrap();
11509        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11510        let sketch_id = sketch_object.id;
11511        let sketch = expect_sketch(sketch_object);
11512        let line_ids = sketch
11513            .segments
11514            .iter()
11515            .copied()
11516            .filter(|segment_id| {
11517                matches!(
11518                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11519                    Some(ObjectKind::Segment {
11520                        segment: Segment::Line(_)
11521                    })
11522                )
11523            })
11524            .collect::<Vec<_>>();
11525
11526        let constraint = Constraint::Distance(Distance {
11527            points: vec![line_ids[0].into(), line_ids[1].into()],
11528            distance: Number {
11529                value: 5.0,
11530                units: NumericSuffix::Mm,
11531            },
11532            label_position: None,
11533            source: Default::default(),
11534        });
11535        let (src_delta, _scene_delta) = frontend
11536            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11537            .await
11538            .unwrap();
11539        assert_eq!(
11540            src_delta.text.as_str(),
11541            "\
11542sketch(on = XY) {
11543  line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11544  line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11545  distance([line1, line2]) == 5mm
11546}
11547"
11548        );
11549
11550        ctx.close().await;
11551        mock_ctx.close().await;
11552    }
11553
11554    #[tokio::test(flavor = "multi_thread")]
11555    async fn test_horizontal_distance_two_points() {
11556        let initial_source = "\
11557sketch(on = XY) {
11558  point(at = [var 1, var 2])
11559  point(at = [var 3, var 4])
11560}
11561";
11562
11563        let program = Program::parse(initial_source).unwrap().0.unwrap();
11564
11565        let mut frontend = FrontendState::new();
11566
11567        let mock_ctx = ExecutorContext::new_mock(None).await;
11568        let version = Version(0);
11569
11570        frontend.program = program.clone();
11571        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11572        frontend.update_state_after_exec(outcome, true);
11573        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11574        let sketch_id = sketch_object.id;
11575        let sketch = expect_sketch(sketch_object);
11576        let point0_id = *sketch.segments.first().unwrap();
11577        let point1_id = *sketch.segments.get(1).unwrap();
11578        let label_position = Point2d {
11579            x: Number {
11580                value: 10.0,
11581                units: NumericSuffix::Mm,
11582            },
11583            y: Number {
11584                value: 11.0,
11585                units: NumericSuffix::Mm,
11586            },
11587        };
11588
11589        let constraint = Constraint::HorizontalDistance(Distance {
11590            points: vec![point0_id.into(), point1_id.into()],
11591            distance: Number {
11592                value: 2.0,
11593                units: NumericSuffix::Mm,
11594            },
11595            label_position: Some(label_position.clone()),
11596            source: Default::default(),
11597        });
11598        let (src_delta, scene_delta) = frontend
11599            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11600            .await
11601            .unwrap();
11602        assert_eq!(
11603            src_delta.text.as_str(),
11604            // The lack indentation is a formatter bug.
11605            "\
11606sketch(on = XY) {
11607  point1 = point(at = [var 1, var 2])
11608  point2 = point(at = [var 3, var 4])
11609  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11610}
11611"
11612        );
11613        assert_eq!(
11614            scene_delta.new_graph.objects.len(),
11615            5,
11616            "{:#?}",
11617            scene_delta.new_graph.objects
11618        );
11619        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11620        let sketch = expect_sketch(sketch_object);
11621        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11622        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11623            panic!("Expected constraint object");
11624        };
11625        let Constraint::HorizontalDistance(distance) = constraint else {
11626            panic!("Expected horizontal distance constraint");
11627        };
11628        assert_eq!(distance.label_position, Some(label_position));
11629
11630        mock_ctx.close().await;
11631    }
11632
11633    #[tokio::test(flavor = "multi_thread")]
11634    async fn test_radius_single_arc_segment() {
11635        let initial_source = "\
11636sketch(on = XY) {
11637  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11638}
11639";
11640
11641        let program = Program::parse(initial_source).unwrap().0.unwrap();
11642
11643        let mut frontend = FrontendState::new();
11644
11645        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11646        let mock_ctx = ExecutorContext::new_mock(None).await;
11647        let version = Version(0);
11648
11649        frontend.hack_set_program(&ctx, program).await.unwrap();
11650        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11651        let sketch_id = sketch_object.id;
11652        let sketch = expect_sketch(sketch_object);
11653        // Find the arc segment (not the points)
11654        let arc_id = sketch
11655            .segments
11656            .iter()
11657            .find(|&seg_id| {
11658                let obj = frontend.scene_graph.objects.get(seg_id.0);
11659                matches!(
11660                    obj.map(|o| &o.kind),
11661                    Some(ObjectKind::Segment {
11662                        segment: Segment::Arc(_)
11663                    })
11664                )
11665            })
11666            .unwrap();
11667
11668        let constraint = Constraint::Radius(Radius {
11669            arc: *arc_id,
11670            radius: Number {
11671                value: 5.0,
11672                units: NumericSuffix::Mm,
11673            },
11674            label_position: None,
11675            source: Default::default(),
11676        });
11677        let (src_delta, scene_delta) = frontend
11678            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11679            .await
11680            .unwrap();
11681        assert_eq!(
11682            src_delta.text.as_str(),
11683            // The lack indentation is a formatter bug.
11684            "\
11685sketch(on = XY) {
11686  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11687  radius(arc1) == 5mm
11688}
11689"
11690        );
11691        assert_eq!(
11692            scene_delta.new_graph.objects.len(),
11693            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11694            "{:#?}",
11695            scene_delta.new_graph.objects
11696        );
11697
11698        ctx.close().await;
11699        mock_ctx.close().await;
11700    }
11701
11702    #[tokio::test(flavor = "multi_thread")]
11703    async fn test_radius_single_arc_segment_with_label_position() {
11704        let initial_source = "\
11705sketch(on = XY) {
11706  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11707}
11708";
11709
11710        let program = Program::parse(initial_source).unwrap().0.unwrap();
11711        let mut frontend = FrontendState::new();
11712        let mock_ctx = ExecutorContext::new_mock(None).await;
11713        let version = Version(0);
11714
11715        frontend.program = program.clone();
11716        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11717        frontend.update_state_after_exec(outcome, true);
11718        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11719        let sketch_id = sketch_object.id;
11720        let sketch = expect_sketch(sketch_object);
11721        let arc_id = sketch
11722            .segments
11723            .iter()
11724            .find(|&seg_id| {
11725                let obj = frontend.scene_graph.objects.get(seg_id.0);
11726                matches!(
11727                    obj.map(|o| &o.kind),
11728                    Some(ObjectKind::Segment {
11729                        segment: Segment::Arc(_)
11730                    })
11731                )
11732            })
11733            .unwrap();
11734
11735        let label_position = Point2d {
11736            x: Number {
11737                value: 10.0,
11738                units: NumericSuffix::Mm,
11739            },
11740            y: Number {
11741                value: 11.0,
11742                units: NumericSuffix::Mm,
11743            },
11744        };
11745        let constraint = Constraint::Radius(Radius {
11746            arc: *arc_id,
11747            radius: Number {
11748                value: 5.0,
11749                units: NumericSuffix::Mm,
11750            },
11751            label_position: Some(label_position.clone()),
11752            source: Default::default(),
11753        });
11754        let (src_delta, scene_delta) = frontend
11755            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11756            .await
11757            .unwrap();
11758        assert_eq!(
11759            src_delta.text.as_str(),
11760            "\
11761sketch(on = XY) {
11762  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11763  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11764}
11765"
11766        );
11767
11768        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11769        let sketch = expect_sketch(sketch_object);
11770        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11771        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11772            panic!("Expected constraint object");
11773        };
11774        let Constraint::Radius(radius) = constraint else {
11775            panic!("Expected radius constraint");
11776        };
11777        assert_eq!(radius.label_position, Some(label_position));
11778
11779        mock_ctx.close().await;
11780    }
11781
11782    #[tokio::test(flavor = "multi_thread")]
11783    async fn test_edit_radius_constraint_label_position() {
11784        let initial_source = "\
11785sketch(on = XY) {
11786  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11787  radius(arc1) == 5mm
11788}
11789";
11790
11791        let program = Program::parse(initial_source).unwrap().0.unwrap();
11792        let mut frontend = FrontendState::new();
11793        let mock_ctx = ExecutorContext::new_mock(None).await;
11794        let version = Version(0);
11795
11796        frontend.program = program.clone();
11797        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11798        frontend.update_state_after_exec(outcome, true);
11799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11800        let sketch_id = sketch_object.id;
11801        let sketch = expect_sketch(sketch_object);
11802        let constraint_id = sketch.constraints[0];
11803        let label_position = Point2d {
11804            x: Number {
11805                value: 10.0,
11806                units: NumericSuffix::Mm,
11807            },
11808            y: Number {
11809                value: 11.0,
11810                units: NumericSuffix::Mm,
11811            },
11812        };
11813
11814        let (src_delta, scene_delta) = frontend
11815            .edit_distance_constraint_label_position(
11816                &mock_ctx,
11817                version,
11818                sketch_id,
11819                constraint_id,
11820                label_position.clone(),
11821                vec![],
11822            )
11823            .await
11824            .unwrap();
11825        assert_eq!(
11826            src_delta.text.as_str(),
11827            "\
11828sketch(on = XY) {
11829  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11830  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11831}
11832"
11833        );
11834
11835        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11836        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11837            panic!("Expected constraint object");
11838        };
11839        let Constraint::Radius(radius) = constraint else {
11840            panic!("Expected radius constraint");
11841        };
11842        assert_eq!(radius.label_position, Some(label_position));
11843
11844        mock_ctx.close().await;
11845    }
11846
11847    #[tokio::test(flavor = "multi_thread")]
11848    async fn test_vertical_distance_two_points() {
11849        let initial_source = "\
11850sketch(on = XY) {
11851  point(at = [var 1, var 2])
11852  point(at = [var 3, var 4])
11853}
11854";
11855
11856        let program = Program::parse(initial_source).unwrap().0.unwrap();
11857
11858        let mut frontend = FrontendState::new();
11859
11860        let mock_ctx = ExecutorContext::new_mock(None).await;
11861        let version = Version(0);
11862
11863        frontend.program = program.clone();
11864        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11865        frontend.update_state_after_exec(outcome, true);
11866        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11867        let sketch_id = sketch_object.id;
11868        let sketch = expect_sketch(sketch_object);
11869        let point0_id = *sketch.segments.first().unwrap();
11870        let point1_id = *sketch.segments.get(1).unwrap();
11871        let label_position = Point2d {
11872            x: Number {
11873                value: 10.0,
11874                units: NumericSuffix::Mm,
11875            },
11876            y: Number {
11877                value: 11.0,
11878                units: NumericSuffix::Mm,
11879            },
11880        };
11881
11882        let constraint = Constraint::VerticalDistance(Distance {
11883            points: vec![point0_id.into(), point1_id.into()],
11884            distance: Number {
11885                value: 2.0,
11886                units: NumericSuffix::Mm,
11887            },
11888            label_position: Some(label_position.clone()),
11889            source: Default::default(),
11890        });
11891        let (src_delta, scene_delta) = frontend
11892            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11893            .await
11894            .unwrap();
11895        assert_eq!(
11896            src_delta.text.as_str(),
11897            // The lack indentation is a formatter bug.
11898            "\
11899sketch(on = XY) {
11900  point1 = point(at = [var 1, var 2])
11901  point2 = point(at = [var 3, var 4])
11902  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11903}
11904"
11905        );
11906        assert_eq!(
11907            scene_delta.new_graph.objects.len(),
11908            5,
11909            "{:#?}",
11910            scene_delta.new_graph.objects
11911        );
11912        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11913        let sketch = expect_sketch(sketch_object);
11914        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11915        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11916            panic!("Expected constraint object");
11917        };
11918        let Constraint::VerticalDistance(distance) = constraint else {
11919            panic!("Expected vertical distance constraint");
11920        };
11921        assert_eq!(distance.label_position, Some(label_position));
11922
11923        mock_ctx.close().await;
11924    }
11925
11926    #[tokio::test(flavor = "multi_thread")]
11927    async fn test_add_fixed_standalone_point() {
11928        let initial_source = "\
11929sketch(on = XY) {
11930  point(at = [var 1, var 2])
11931}
11932";
11933
11934        let program = Program::parse(initial_source).unwrap().0.unwrap();
11935
11936        let mut frontend = FrontendState::new();
11937
11938        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11939        let mock_ctx = ExecutorContext::new_mock(None).await;
11940        let version = Version(0);
11941
11942        frontend.hack_set_program(&ctx, program).await.unwrap();
11943        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944        let sketch_id = sketch_object.id;
11945        let sketch = expect_sketch(sketch_object);
11946        let point_id = *sketch.segments.first().unwrap();
11947
11948        let (src_delta, scene_delta) = frontend
11949            .add_constraint(
11950                &mock_ctx,
11951                version,
11952                sketch_id,
11953                Constraint::Fixed(Fixed {
11954                    points: vec![FixedPoint {
11955                        point: point_id,
11956                        position: Point2d {
11957                            x: Number {
11958                                value: 2.0,
11959                                units: NumericSuffix::Mm,
11960                            },
11961                            y: Number {
11962                                value: 3.0,
11963                                units: NumericSuffix::Mm,
11964                            },
11965                        },
11966                    }],
11967                }),
11968            )
11969            .await
11970            .unwrap();
11971        assert_eq!(
11972            src_delta.text.as_str(),
11973            "\
11974sketch(on = XY) {
11975  point1 = point(at = [var 2, var 3])
11976  fixed([point1, [2mm, 3mm]])
11977}
11978"
11979        );
11980        assert_eq!(
11981            scene_delta.new_graph.objects.len(),
11982            4,
11983            "{:#?}",
11984            scene_delta.new_graph.objects
11985        );
11986
11987        ctx.close().await;
11988        mock_ctx.close().await;
11989    }
11990
11991    #[tokio::test(flavor = "multi_thread")]
11992    async fn test_add_fixed_multiple_points() {
11993        let initial_source = "\
11994sketch(on = XY) {
11995  point(at = [var 1, var 2])
11996  point(at = [var 3, var 4])
11997}
11998";
11999
12000        let program = Program::parse(initial_source).unwrap().0.unwrap();
12001
12002        let mut frontend = FrontendState::new();
12003
12004        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12005        let mock_ctx = ExecutorContext::new_mock(None).await;
12006        let version = Version(0);
12007
12008        frontend.hack_set_program(&ctx, program).await.unwrap();
12009        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12010        let sketch_id = sketch_object.id;
12011        let sketch = expect_sketch(sketch_object);
12012        let point0_id = *sketch.segments.first().unwrap();
12013        let point1_id = *sketch.segments.get(1).unwrap();
12014
12015        let (src_delta, scene_delta) = frontend
12016            .add_constraint(
12017                &mock_ctx,
12018                version,
12019                sketch_id,
12020                Constraint::Fixed(Fixed {
12021                    points: vec![
12022                        FixedPoint {
12023                            point: point0_id,
12024                            position: Point2d {
12025                                x: Number {
12026                                    value: 2.0,
12027                                    units: NumericSuffix::Mm,
12028                                },
12029                                y: Number {
12030                                    value: 3.0,
12031                                    units: NumericSuffix::Mm,
12032                                },
12033                            },
12034                        },
12035                        FixedPoint {
12036                            point: point1_id,
12037                            position: Point2d {
12038                                x: Number {
12039                                    value: 4.0,
12040                                    units: NumericSuffix::Mm,
12041                                },
12042                                y: Number {
12043                                    value: 5.0,
12044                                    units: NumericSuffix::Mm,
12045                                },
12046                            },
12047                        },
12048                    ],
12049                }),
12050            )
12051            .await
12052            .unwrap();
12053        assert_eq!(
12054            src_delta.text.as_str(),
12055            "\
12056sketch(on = XY) {
12057  point1 = point(at = [var 2, var 3])
12058  point2 = point(at = [var 4, var 5])
12059  fixed([point1, [2mm, 3mm]])
12060  fixed([point2, [4mm, 5mm]])
12061}
12062"
12063        );
12064        assert_eq!(
12065            scene_delta.new_graph.objects.len(),
12066            6,
12067            "{:#?}",
12068            scene_delta.new_graph.objects
12069        );
12070
12071        ctx.close().await;
12072        mock_ctx.close().await;
12073    }
12074
12075    #[tokio::test(flavor = "multi_thread")]
12076    async fn test_add_fixed_owned_point() {
12077        let initial_source = "\
12078sketch(on = XY) {
12079  line(start = [var 1, var 2], end = [var 3, var 4])
12080}
12081";
12082
12083        let program = Program::parse(initial_source).unwrap().0.unwrap();
12084
12085        let mut frontend = FrontendState::new();
12086
12087        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12088        let mock_ctx = ExecutorContext::new_mock(None).await;
12089        let version = Version(0);
12090
12091        frontend.hack_set_program(&ctx, program).await.unwrap();
12092        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12093        let sketch_id = sketch_object.id;
12094        let sketch = expect_sketch(sketch_object);
12095        let line_start_id = *sketch.segments.first().unwrap();
12096
12097        let (src_delta, scene_delta) = frontend
12098            .add_constraint(
12099                &mock_ctx,
12100                version,
12101                sketch_id,
12102                Constraint::Fixed(Fixed {
12103                    points: vec![FixedPoint {
12104                        point: line_start_id,
12105                        position: Point2d {
12106                            x: Number {
12107                                value: 2.0,
12108                                units: NumericSuffix::Mm,
12109                            },
12110                            y: Number {
12111                                value: 3.0,
12112                                units: NumericSuffix::Mm,
12113                            },
12114                        },
12115                    }],
12116                }),
12117            )
12118            .await
12119            .unwrap();
12120        assert_eq!(
12121            src_delta.text.as_str(),
12122            "\
12123sketch(on = XY) {
12124  line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12125  fixed([line1.start, [2mm, 3mm]])
12126}
12127"
12128        );
12129        assert_eq!(
12130            scene_delta.new_graph.objects.len(),
12131            6,
12132            "{:#?}",
12133            scene_delta.new_graph.objects
12134        );
12135
12136        ctx.close().await;
12137        mock_ctx.close().await;
12138    }
12139
12140    #[tokio::test(flavor = "multi_thread")]
12141    async fn test_radius_error_cases() {
12142        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12143        let mock_ctx = ExecutorContext::new_mock(None).await;
12144        let version = Version(0);
12145
12146        // Test: Single point should error
12147        let initial_source_point = "\
12148sketch(on = XY) {
12149  point(at = [var 1, var 2])
12150}
12151";
12152        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12153        let mut frontend_point = FrontendState::new();
12154        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12155        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12156        let sketch_id_point = sketch_object_point.id;
12157        let sketch_point = expect_sketch(sketch_object_point);
12158        let point_id = *sketch_point.segments.first().unwrap();
12159
12160        let constraint_point = Constraint::Radius(Radius {
12161            arc: point_id,
12162            radius: Number {
12163                value: 5.0,
12164                units: NumericSuffix::Mm,
12165            },
12166            label_position: None,
12167            source: Default::default(),
12168        });
12169        let result_point = frontend_point
12170            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12171            .await;
12172        assert!(result_point.is_err(), "Single point should error for radius");
12173
12174        // Test: Single line segment should error (only arc segments supported)
12175        let initial_source_line = "\
12176sketch(on = XY) {
12177  line(start = [var 1, var 2], end = [var 3, var 4])
12178}
12179";
12180        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12181        let mut frontend_line = FrontendState::new();
12182        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12183        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12184        let sketch_id_line = sketch_object_line.id;
12185        let sketch_line = expect_sketch(sketch_object_line);
12186        let line_id = *sketch_line.segments.first().unwrap();
12187
12188        let constraint_line = Constraint::Radius(Radius {
12189            arc: line_id,
12190            radius: Number {
12191                value: 5.0,
12192                units: NumericSuffix::Mm,
12193            },
12194            label_position: None,
12195            source: Default::default(),
12196        });
12197        let result_line = frontend_line
12198            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12199            .await;
12200        assert!(result_line.is_err(), "Single line segment should error for radius");
12201
12202        ctx.close().await;
12203        mock_ctx.close().await;
12204    }
12205
12206    #[tokio::test(flavor = "multi_thread")]
12207    async fn test_diameter_single_arc_segment() {
12208        let initial_source = "\
12209sketch(on = XY) {
12210  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12211}
12212";
12213
12214        let program = Program::parse(initial_source).unwrap().0.unwrap();
12215
12216        let mut frontend = FrontendState::new();
12217
12218        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12219        let mock_ctx = ExecutorContext::new_mock(None).await;
12220        let version = Version(0);
12221
12222        frontend.hack_set_program(&ctx, program).await.unwrap();
12223        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12224        let sketch_id = sketch_object.id;
12225        let sketch = expect_sketch(sketch_object);
12226        // Find the arc segment (not the points)
12227        let arc_id = sketch
12228            .segments
12229            .iter()
12230            .find(|&seg_id| {
12231                let obj = frontend.scene_graph.objects.get(seg_id.0);
12232                matches!(
12233                    obj.map(|o| &o.kind),
12234                    Some(ObjectKind::Segment {
12235                        segment: Segment::Arc(_)
12236                    })
12237                )
12238            })
12239            .unwrap();
12240
12241        let constraint = Constraint::Diameter(Diameter {
12242            arc: *arc_id,
12243            diameter: Number {
12244                value: 10.0,
12245                units: NumericSuffix::Mm,
12246            },
12247            label_position: None,
12248            source: Default::default(),
12249        });
12250        let (src_delta, scene_delta) = frontend
12251            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12252            .await
12253            .unwrap();
12254        assert_eq!(
12255            src_delta.text.as_str(),
12256            // The lack indentation is a formatter bug.
12257            "\
12258sketch(on = XY) {
12259  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12260  diameter(arc1) == 10mm
12261}
12262"
12263        );
12264        assert_eq!(
12265            scene_delta.new_graph.objects.len(),
12266            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12267            "{:#?}",
12268            scene_delta.new_graph.objects
12269        );
12270
12271        ctx.close().await;
12272        mock_ctx.close().await;
12273    }
12274
12275    #[tokio::test(flavor = "multi_thread")]
12276    async fn test_diameter_single_arc_segment_with_label_position() {
12277        let initial_source = "\
12278sketch(on = XY) {
12279  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12280}
12281";
12282
12283        let program = Program::parse(initial_source).unwrap().0.unwrap();
12284        let mut frontend = FrontendState::new();
12285        let mock_ctx = ExecutorContext::new_mock(None).await;
12286        let version = Version(0);
12287
12288        frontend.program = program.clone();
12289        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12290        frontend.update_state_after_exec(outcome, true);
12291        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12292        let sketch_id = sketch_object.id;
12293        let sketch = expect_sketch(sketch_object);
12294        let arc_id = sketch
12295            .segments
12296            .iter()
12297            .find(|&seg_id| {
12298                let obj = frontend.scene_graph.objects.get(seg_id.0);
12299                matches!(
12300                    obj.map(|o| &o.kind),
12301                    Some(ObjectKind::Segment {
12302                        segment: Segment::Arc(_)
12303                    })
12304                )
12305            })
12306            .unwrap();
12307
12308        let label_position = Point2d {
12309            x: Number {
12310                value: 10.0,
12311                units: NumericSuffix::Mm,
12312            },
12313            y: Number {
12314                value: 11.0,
12315                units: NumericSuffix::Mm,
12316            },
12317        };
12318        let constraint = Constraint::Diameter(Diameter {
12319            arc: *arc_id,
12320            diameter: Number {
12321                value: 10.0,
12322                units: NumericSuffix::Mm,
12323            },
12324            label_position: Some(label_position.clone()),
12325            source: Default::default(),
12326        });
12327        let (src_delta, scene_delta) = frontend
12328            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12329            .await
12330            .unwrap();
12331        assert_eq!(
12332            src_delta.text.as_str(),
12333            "\
12334sketch(on = XY) {
12335  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12336  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12337}
12338"
12339        );
12340
12341        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12342        let sketch = expect_sketch(sketch_object);
12343        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12344        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12345            panic!("Expected constraint object");
12346        };
12347        let Constraint::Diameter(diameter) = constraint else {
12348            panic!("Expected diameter constraint");
12349        };
12350        assert_eq!(diameter.label_position, Some(label_position));
12351
12352        mock_ctx.close().await;
12353    }
12354
12355    #[tokio::test(flavor = "multi_thread")]
12356    async fn test_edit_diameter_constraint_label_position() {
12357        let initial_source = "\
12358sketch(on = XY) {
12359  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12360  diameter(arc1) == 10mm
12361}
12362";
12363
12364        let program = Program::parse(initial_source).unwrap().0.unwrap();
12365        let mut frontend = FrontendState::new();
12366        let mock_ctx = ExecutorContext::new_mock(None).await;
12367        let version = Version(0);
12368
12369        frontend.program = program.clone();
12370        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12371        frontend.update_state_after_exec(outcome, true);
12372        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12373        let sketch_id = sketch_object.id;
12374        let sketch = expect_sketch(sketch_object);
12375        let constraint_id = sketch.constraints[0];
12376        let label_position = Point2d {
12377            x: Number {
12378                value: 10.0,
12379                units: NumericSuffix::Mm,
12380            },
12381            y: Number {
12382                value: 11.0,
12383                units: NumericSuffix::Mm,
12384            },
12385        };
12386
12387        let (src_delta, scene_delta) = frontend
12388            .edit_distance_constraint_label_position(
12389                &mock_ctx,
12390                version,
12391                sketch_id,
12392                constraint_id,
12393                label_position.clone(),
12394                vec![],
12395            )
12396            .await
12397            .unwrap();
12398        assert_eq!(
12399            src_delta.text.as_str(),
12400            "\
12401sketch(on = XY) {
12402  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12403  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12404}
12405"
12406        );
12407
12408        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12409        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12410            panic!("Expected constraint object");
12411        };
12412        let Constraint::Diameter(diameter) = constraint else {
12413            panic!("Expected diameter constraint");
12414        };
12415        assert_eq!(diameter.label_position, Some(label_position));
12416
12417        mock_ctx.close().await;
12418    }
12419
12420    #[tokio::test(flavor = "multi_thread")]
12421    async fn test_diameter_error_cases() {
12422        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12423        let mock_ctx = ExecutorContext::new_mock(None).await;
12424        let version = Version(0);
12425
12426        // Test: Single point should error
12427        let initial_source_point = "\
12428sketch(on = XY) {
12429  point(at = [var 1, var 2])
12430}
12431";
12432        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12433        let mut frontend_point = FrontendState::new();
12434        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12435        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12436        let sketch_id_point = sketch_object_point.id;
12437        let sketch_point = expect_sketch(sketch_object_point);
12438        let point_id = *sketch_point.segments.first().unwrap();
12439
12440        let constraint_point = Constraint::Diameter(Diameter {
12441            arc: point_id,
12442            diameter: Number {
12443                value: 10.0,
12444                units: NumericSuffix::Mm,
12445            },
12446            label_position: None,
12447            source: Default::default(),
12448        });
12449        let result_point = frontend_point
12450            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12451            .await;
12452        assert!(result_point.is_err(), "Single point should error for diameter");
12453
12454        // Test: Single line segment should error (only arc segments supported)
12455        let initial_source_line = "\
12456sketch(on = XY) {
12457  line(start = [var 1, var 2], end = [var 3, var 4])
12458}
12459";
12460        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12461        let mut frontend_line = FrontendState::new();
12462        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12463        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12464        let sketch_id_line = sketch_object_line.id;
12465        let sketch_line = expect_sketch(sketch_object_line);
12466        let line_id = *sketch_line.segments.first().unwrap();
12467
12468        let constraint_line = Constraint::Diameter(Diameter {
12469            arc: line_id,
12470            diameter: Number {
12471                value: 10.0,
12472                units: NumericSuffix::Mm,
12473            },
12474            label_position: None,
12475            source: Default::default(),
12476        });
12477        let result_line = frontend_line
12478            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12479            .await;
12480        assert!(result_line.is_err(), "Single line segment should error for diameter");
12481
12482        ctx.close().await;
12483        mock_ctx.close().await;
12484    }
12485
12486    #[tokio::test(flavor = "multi_thread")]
12487    async fn test_line_horizontal() {
12488        let initial_source = "\
12489sketch(on = XY) {
12490  line(start = [var 1, var 2], end = [var 3, var 4])
12491}
12492";
12493
12494        let program = Program::parse(initial_source).unwrap().0.unwrap();
12495
12496        let mut frontend = FrontendState::new();
12497
12498        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12499        let mock_ctx = ExecutorContext::new_mock(None).await;
12500        let version = Version(0);
12501
12502        frontend.hack_set_program(&ctx, program).await.unwrap();
12503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12504        let sketch_id = sketch_object.id;
12505        let sketch = expect_sketch(sketch_object);
12506        let line1_id = *sketch.segments.get(2).unwrap();
12507
12508        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12509        let (src_delta, scene_delta) = frontend
12510            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12511            .await
12512            .unwrap();
12513        assert_eq!(
12514            src_delta.text.as_str(),
12515            "\
12516sketch(on = XY) {
12517  line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12518  horizontal(line1)
12519}
12520"
12521        );
12522        assert_eq!(
12523            scene_delta.new_graph.objects.len(),
12524            6,
12525            "{:#?}",
12526            scene_delta.new_graph.objects
12527        );
12528
12529        ctx.close().await;
12530        mock_ctx.close().await;
12531    }
12532
12533    #[tokio::test(flavor = "multi_thread")]
12534    async fn test_control_point_spline_edge_horizontal() {
12535        let initial_source = "\
12536@settings(experimentalFeatures = allow)
12537splineSketch = sketch(on = XY) {
12538  controlPointSpline1 = controlPointSpline(points = [
12539    [var 0mm, var 0mm],
12540    [var 10mm, var 20mm],
12541    [var 20mm, var 0mm],
12542  ])
12543}
12544";
12545
12546        let program = Program::parse(initial_source).unwrap().0.unwrap();
12547
12548        let mut frontend = FrontendState::new();
12549
12550        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12551        let mock_ctx = ExecutorContext::new_mock(None).await;
12552        let version = Version(0);
12553
12554        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12555        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12556        let sketch_id = sketch_object.id;
12557        let sketch = expect_sketch(sketch_object);
12558        let spline_id = sketch
12559            .segments
12560            .iter()
12561            .copied()
12562            .find(|seg_id| {
12563                matches!(
12564                    &frontend.scene_graph.objects[seg_id.0].kind,
12565                    ObjectKind::Segment {
12566                        segment: Segment::ControlPointSpline(_)
12567                    }
12568                )
12569            })
12570            .expect("Expected a control point spline segment in sketch");
12571        let edge_id = frontend
12572            .scene_graph
12573            .objects
12574            .iter()
12575            .find_map(|obj| match &obj.kind {
12576                ObjectKind::Segment {
12577                    segment: Segment::Line(line),
12578                } if line.owner == Some(spline_id) => Some(obj.id),
12579                _ => None,
12580            })
12581            .expect("Expected an owned control-polygon edge");
12582
12583        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12584        let (src_delta, _) = frontend
12585            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12586            .await
12587            .unwrap();
12588        assert!(
12589            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12590            "Expected horizontal constraint on spline edge, got: {}",
12591            src_delta.text
12592        );
12593
12594        ctx.close().await;
12595        mock_ctx.close().await;
12596    }
12597
12598    #[tokio::test(flavor = "multi_thread")]
12599    async fn test_control_point_spline_edge_angle() {
12600        let initial_source = "\
12601@settings(experimentalFeatures = allow)
12602splineSketch = sketch(on = XY) {
12603  controlPointSpline1 = controlPointSpline(points = [
12604    [var 0mm, var 0mm],
12605    [var 10mm, var 20mm],
12606    [var 20mm, var 0mm],
12607  ])
12608
12609  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12610}
12611";
12612
12613        let program = Program::parse(initial_source).unwrap().0.unwrap();
12614
12615        let mut frontend = FrontendState::new();
12616
12617        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12618        let mock_ctx = ExecutorContext::new_mock(None).await;
12619        let version = Version(0);
12620
12621        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12622        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12623        let sketch_id = sketch_object.id;
12624        let sketch = expect_sketch(sketch_object);
12625        let spline_id = sketch
12626            .segments
12627            .iter()
12628            .copied()
12629            .find(|seg_id| {
12630                matches!(
12631                    &frontend.scene_graph.objects[seg_id.0].kind,
12632                    ObjectKind::Segment {
12633                        segment: Segment::ControlPointSpline(_)
12634                    }
12635                )
12636            })
12637            .expect("Expected a control point spline segment in sketch");
12638        let edge_id = frontend
12639            .scene_graph
12640            .objects
12641            .iter()
12642            .find_map(|obj| match &obj.kind {
12643                ObjectKind::Segment {
12644                    segment: Segment::Line(line),
12645                } if line.owner == Some(spline_id) => Some(obj.id),
12646                _ => None,
12647            })
12648            .expect("Expected an owned control-polygon edge");
12649        let line1_id = frontend
12650            .scene_graph
12651            .objects
12652            .iter()
12653            .find_map(|obj| match &obj.kind {
12654                ObjectKind::Segment {
12655                    segment: Segment::Line(line),
12656                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12657                _ => None,
12658            })
12659            .or_else(|| {
12660                sketch.segments.iter().copied().find(|seg_id| {
12661                    matches!(
12662                        &frontend.scene_graph.objects[seg_id.0].kind,
12663                        ObjectKind::Segment {
12664                            segment: Segment::Line(line),
12665                        } if line.owner.is_none()
12666                    )
12667                })
12668            })
12669            .expect("Expected a standalone line segment in sketch");
12670
12671        let constraint = Constraint::Angle(Angle {
12672            lines: vec![line1_id, edge_id],
12673            angle: Number {
12674                value: 30.0,
12675                units: NumericSuffix::Deg,
12676            },
12677            source: Default::default(),
12678        });
12679        let (src_delta, _) = frontend
12680            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12681            .await
12682            .unwrap();
12683        assert!(
12684            src_delta
12685                .text
12686                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12687            "Expected angle constraint on spline edge, got: {}",
12688            src_delta.text
12689        );
12690
12691        ctx.close().await;
12692        mock_ctx.close().await;
12693    }
12694
12695    #[tokio::test(flavor = "multi_thread")]
12696    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12697        let initial_source = "\
12698@settings(experimentalFeatures = allow)
12699splineSketch = sketch(on = XY) {
12700  spline1 = controlPointSpline(points = [
12701    [var 0mm, var 0mm],
12702    [var 10mm, var 20mm],
12703    [var 20mm, var 0mm],
12704  ])
12705  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12706  coincident([spline1.controls[1], spline1.edges[0]])
12707  coincident([spline1.controls[0], line1])
12708}
12709";
12710
12711        let program = Program::parse(initial_source).unwrap().0.unwrap();
12712
12713        let mut frontend = FrontendState::new();
12714
12715        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12716        let mock_ctx = ExecutorContext::new_mock(None).await;
12717
12718        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12719
12720        let ui_scene_graph = frontend.scene_graph_for_ui();
12721        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12722        let sketch = expect_sketch(sketch_object);
12723
12724        assert_eq!(
12725            sketch.constraints.len(),
12726            1,
12727            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12728        );
12729
12730        let visible_constraints = ui_scene_graph
12731            .objects
12732            .iter()
12733            .filter_map(|object| match &object.kind {
12734                ObjectKind::Constraint {
12735                    constraint: Constraint::Coincident(coincident),
12736                } => Some(coincident.clone()),
12737                _ => None,
12738            })
12739            .collect::<Vec<_>>();
12740
12741        assert_eq!(
12742            visible_constraints.len(),
12743            1,
12744            "Expected only one coincident constraint object in the UI scene graph"
12745        );
12746        assert_eq!(
12747            visible_constraints[0].get_segments().len(),
12748            2,
12749            "Expected the remaining visible coincident constraint to reference two segments"
12750        );
12751
12752        ctx.close().await;
12753        mock_ctx.close().await;
12754    }
12755
12756    #[tokio::test(flavor = "multi_thread")]
12757    async fn test_edit_control_point_spline_can_append_control_point() {
12758        let initial_source = "\
12759@settings(experimentalFeatures = allow)
12760splineSketch = sketch(on = XY) {
12761  controlPointSpline(points = [
12762    [var 0mm, var 0mm],
12763    [var 10mm, var 20mm],
12764    [var 20mm, var 0mm],
12765  ])
12766}
12767";
12768
12769        let program = Program::parse(initial_source).unwrap().0.unwrap();
12770
12771        let mut frontend = FrontendState::new();
12772
12773        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12774        let mock_ctx = ExecutorContext::new_mock(None).await;
12775        let version = Version(0);
12776
12777        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12778        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12779        let sketch_id = sketch_object.id;
12780        let sketch = expect_sketch(sketch_object);
12781        let spline_id = sketch
12782            .segments
12783            .iter()
12784            .copied()
12785            .find(|seg_id| {
12786                matches!(
12787                    &frontend.scene_graph.objects[seg_id.0].kind,
12788                    ObjectKind::Segment {
12789                        segment: Segment::ControlPointSpline(_)
12790                    }
12791                )
12792            })
12793            .expect("Expected a control point spline segment in sketch");
12794
12795        let ctor = ControlPointSplineCtor {
12796            points: vec![
12797                Point2d {
12798                    x: Expr::Var(Number {
12799                        value: 0.0,
12800                        units: NumericSuffix::Mm,
12801                    }),
12802                    y: Expr::Var(Number {
12803                        value: 0.0,
12804                        units: NumericSuffix::Mm,
12805                    }),
12806                },
12807                Point2d {
12808                    x: Expr::Var(Number {
12809                        value: 10.0,
12810                        units: NumericSuffix::Mm,
12811                    }),
12812                    y: Expr::Var(Number {
12813                        value: 20.0,
12814                        units: NumericSuffix::Mm,
12815                    }),
12816                },
12817                Point2d {
12818                    x: Expr::Var(Number {
12819                        value: 20.0,
12820                        units: NumericSuffix::Mm,
12821                    }),
12822                    y: Expr::Var(Number {
12823                        value: 0.0,
12824                        units: NumericSuffix::Mm,
12825                    }),
12826                },
12827                Point2d {
12828                    x: Expr::Var(Number {
12829                        value: 30.0,
12830                        units: NumericSuffix::Mm,
12831                    }),
12832                    y: Expr::Var(Number {
12833                        value: 10.0,
12834                        units: NumericSuffix::Mm,
12835                    }),
12836                },
12837            ],
12838            construction: None,
12839        };
12840
12841        let segments = vec![ExistingSegmentCtor {
12842            id: spline_id,
12843            ctor: SegmentCtor::ControlPointSpline(ctor),
12844        }];
12845        let (src_delta, scene_delta) = frontend
12846            .edit_segments(&mock_ctx, version, sketch_id, segments)
12847            .await
12848            .unwrap();
12849
12850        assert!(
12851            src_delta.text.contains("[var 30mm, var 10mm]"),
12852            "Expected appended spline control point in source, got: {}",
12853            src_delta.text
12854        );
12855
12856        assert!(
12857            scene_delta.invalidates_ids,
12858            "Expected appending a spline control point to invalidate ids"
12859        );
12860        let updated_spline = scene_delta
12861            .new_graph
12862            .objects
12863            .iter()
12864            .find_map(|obj| match &obj.kind {
12865                ObjectKind::Segment {
12866                    segment: Segment::ControlPointSpline(updated_spline),
12867                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12868                _ => None,
12869            })
12870            .expect("Expected edited scene graph to contain a four-point control point spline");
12871        assert_eq!(
12872            updated_spline.controls.len(),
12873            4,
12874            "Expected edited spline to expose four control points"
12875        );
12876
12877        ctx.close().await;
12878        mock_ctx.close().await;
12879    }
12880
12881    #[tokio::test(flavor = "multi_thread")]
12882    async fn test_line_vertical() {
12883        let initial_source = "\
12884sketch(on = XY) {
12885  line(start = [var 1, var 2], end = [var 3, var 4])
12886}
12887";
12888
12889        let program = Program::parse(initial_source).unwrap().0.unwrap();
12890
12891        let mut frontend = FrontendState::new();
12892
12893        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12894        let mock_ctx = ExecutorContext::new_mock(None).await;
12895        let version = Version(0);
12896
12897        frontend.hack_set_program(&ctx, program).await.unwrap();
12898        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12899        let sketch_id = sketch_object.id;
12900        let sketch = expect_sketch(sketch_object);
12901        let line1_id = *sketch.segments.get(2).unwrap();
12902
12903        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12904        let (src_delta, scene_delta) = frontend
12905            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12906            .await
12907            .unwrap();
12908        assert_eq!(
12909            src_delta.text.as_str(),
12910            "\
12911sketch(on = XY) {
12912  line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12913  vertical(line1)
12914}
12915"
12916        );
12917        assert_eq!(
12918            scene_delta.new_graph.objects.len(),
12919            6,
12920            "{:#?}",
12921            scene_delta.new_graph.objects
12922        );
12923
12924        ctx.close().await;
12925        mock_ctx.close().await;
12926    }
12927
12928    #[tokio::test(flavor = "multi_thread")]
12929    async fn test_points_vertical() {
12930        let initial_source = "\
12931sketch001 = sketch(on = XY) {
12932  p0 = point(at = [var -2.23mm, var 3.1mm])
12933  pf = point(at = [4, 4])
12934}
12935";
12936
12937        let program = Program::parse(initial_source).unwrap().0.unwrap();
12938
12939        let mut frontend = FrontendState::new();
12940
12941        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12942        let mock_ctx = ExecutorContext::new_mock(None).await;
12943        let version = Version(0);
12944
12945        frontend.hack_set_program(&ctx, program).await.unwrap();
12946        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12947        let sketch_id = sketch_object.id;
12948        let sketch = expect_sketch(sketch_object);
12949        let point_ids = vec![
12950            sketch.segments.first().unwrap().to_owned(),
12951            sketch.segments.get(1).unwrap().to_owned(),
12952        ];
12953
12954        let constraint = Constraint::Vertical(Vertical::Points {
12955            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12956        });
12957        let (src_delta, scene_delta) = frontend
12958            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12959            .await
12960            .unwrap();
12961        assert_eq!(
12962            src_delta.text.as_str(),
12963            "\
12964sketch001 = sketch(on = XY) {
12965  p0 = point(at = [var 4mm, var 3.1mm])
12966  pf = point(at = [4, 4])
12967  vertical([p0, pf])
12968}
12969"
12970        );
12971        assert_eq!(
12972            scene_delta.new_graph.objects.len(),
12973            5,
12974            "{:#?}",
12975            scene_delta.new_graph.objects
12976        );
12977
12978        ctx.close().await;
12979        mock_ctx.close().await;
12980    }
12981
12982    #[tokio::test(flavor = "multi_thread")]
12983    async fn test_points_horizontal() {
12984        let initial_source = "\
12985sketch001 = sketch(on = XY) {
12986  p0 = point(at = [var -2.23mm, var 3.1mm])
12987  pf = point(at = [4, 4])
12988}
12989";
12990
12991        let program = Program::parse(initial_source).unwrap().0.unwrap();
12992
12993        let mut frontend = FrontendState::new();
12994
12995        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12996        let mock_ctx = ExecutorContext::new_mock(None).await;
12997        let version = Version(0);
12998
12999        frontend.hack_set_program(&ctx, program).await.unwrap();
13000        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13001        let sketch_id = sketch_object.id;
13002        let sketch = expect_sketch(sketch_object);
13003        let point_ids = vec![
13004            sketch.segments.first().unwrap().to_owned(),
13005            sketch.segments.get(1).unwrap().to_owned(),
13006        ];
13007
13008        let constraint = Constraint::Horizontal(Horizontal::Points {
13009            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13010        });
13011        let (src_delta, scene_delta) = frontend
13012            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13013            .await
13014            .unwrap();
13015        assert_eq!(
13016            src_delta.text.as_str(),
13017            "\
13018sketch001 = sketch(on = XY) {
13019  p0 = point(at = [var -2.23mm, var 4mm])
13020  pf = point(at = [4, 4])
13021  horizontal([p0, pf])
13022}
13023"
13024        );
13025        assert_eq!(
13026            scene_delta.new_graph.objects.len(),
13027            5,
13028            "{:#?}",
13029            scene_delta.new_graph.objects
13030        );
13031
13032        ctx.close().await;
13033        mock_ctx.close().await;
13034    }
13035
13036    #[tokio::test(flavor = "multi_thread")]
13037    async fn test_point_horizontal_with_origin() {
13038        let initial_source = "\
13039sketch001 = sketch(on = XY) {
13040  p0 = point(at = [var -2.23mm, var 3.1mm])
13041}
13042";
13043
13044        let program = Program::parse(initial_source).unwrap().0.unwrap();
13045
13046        let mut frontend = FrontendState::new();
13047
13048        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13049        let mock_ctx = ExecutorContext::new_mock(None).await;
13050        let version = Version(0);
13051
13052        frontend.hack_set_program(&ctx, program).await.unwrap();
13053        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13054        let sketch_id = sketch_object.id;
13055        let sketch = expect_sketch(sketch_object);
13056        let point_id = *sketch.segments.first().unwrap();
13057
13058        let constraint = Constraint::Horizontal(Horizontal::Points {
13059            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13060        });
13061        let (src_delta, scene_delta) = frontend
13062            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13063            .await
13064            .unwrap();
13065        assert_eq!(
13066            src_delta.text.as_str(),
13067            "\
13068sketch001 = sketch(on = XY) {
13069  p0 = point(at = [var -2.23mm, var 0mm])
13070  horizontal([p0, ORIGIN])
13071}
13072"
13073        );
13074        assert_eq!(
13075            scene_delta.new_graph.objects.len(),
13076            4,
13077            "{:#?}",
13078            scene_delta.new_graph.objects
13079        );
13080
13081        ctx.close().await;
13082        mock_ctx.close().await;
13083    }
13084
13085    #[tokio::test(flavor = "multi_thread")]
13086    async fn test_lines_equal_length() {
13087        let initial_source = "\
13088sketch(on = XY) {
13089  line(start = [var 1, var 2], end = [var 3, var 4])
13090  line(start = [var 5, var 6], end = [var 7, var 8])
13091}
13092";
13093
13094        let program = Program::parse(initial_source).unwrap().0.unwrap();
13095
13096        let mut frontend = FrontendState::new();
13097
13098        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13099        let mock_ctx = ExecutorContext::new_mock(None).await;
13100        let version = Version(0);
13101
13102        frontend.hack_set_program(&ctx, program).await.unwrap();
13103        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13104        let sketch_id = sketch_object.id;
13105        let sketch = expect_sketch(sketch_object);
13106        let line1_id = *sketch.segments.get(2).unwrap();
13107        let line2_id = *sketch.segments.get(5).unwrap();
13108
13109        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13110            lines: vec![line1_id, line2_id],
13111        });
13112        let (src_delta, scene_delta) = frontend
13113            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13114            .await
13115            .unwrap();
13116        assert_eq!(
13117            src_delta.text.as_str(),
13118            "\
13119sketch(on = XY) {
13120  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13121  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13122  equalLength([line1, line2])
13123}
13124"
13125        );
13126        assert_eq!(
13127            scene_delta.new_graph.objects.len(),
13128            9,
13129            "{:#?}",
13130            scene_delta.new_graph.objects
13131        );
13132
13133        ctx.close().await;
13134        mock_ctx.close().await;
13135    }
13136
13137    #[tokio::test(flavor = "multi_thread")]
13138    async fn test_add_constraint_multi_line_equal_length() {
13139        let initial_source = "\
13140sketch(on = XY) {
13141  line(start = [var 1, var 2], end = [var 3, var 4])
13142  line(start = [var 5, var 6], end = [var 7, var 8])
13143  line(start = [var 9, var 10], end = [var 11, var 12])
13144}
13145";
13146
13147        let program = Program::parse(initial_source).unwrap().0.unwrap();
13148
13149        let mut frontend = FrontendState::new();
13150        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13151        let mock_ctx = ExecutorContext::new_mock(None).await;
13152        let version = Version(0);
13153
13154        frontend.hack_set_program(&ctx, program).await.unwrap();
13155        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13156        let sketch_id = sketch_object.id;
13157        let sketch = expect_sketch(sketch_object);
13158        let line1_id = *sketch.segments.get(2).unwrap();
13159        let line2_id = *sketch.segments.get(5).unwrap();
13160        let line3_id = *sketch.segments.get(8).unwrap();
13161
13162        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13163            lines: vec![line1_id, line2_id, line3_id],
13164        });
13165        let (src_delta, scene_delta) = frontend
13166            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13167            .await
13168            .unwrap();
13169        assert_eq!(
13170            src_delta.text.as_str(),
13171            "\
13172sketch(on = XY) {
13173  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13174  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13175  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13176  equalLength([line1, line2, line3])
13177}
13178"
13179        );
13180        let constraints = scene_delta
13181            .new_graph
13182            .objects
13183            .iter()
13184            .filter_map(|obj| {
13185                let ObjectKind::Constraint { constraint } = &obj.kind else {
13186                    return None;
13187                };
13188                Some(constraint)
13189            })
13190            .collect::<Vec<_>>();
13191
13192        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13193        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13194            panic!("expected equal length constraint, got {:?}", constraints[0]);
13195        };
13196        assert_eq!(lines_equal_length.lines.len(), 3);
13197
13198        ctx.close().await;
13199        mock_ctx.close().await;
13200    }
13201
13202    #[tokio::test(flavor = "multi_thread")]
13203    async fn test_lines_parallel() {
13204        let initial_source = "\
13205sketch(on = XY) {
13206  line(start = [var 1, var 2], end = [var 3, var 4])
13207  line(start = [var 5, var 6], end = [var 7, var 8])
13208}
13209";
13210
13211        let program = Program::parse(initial_source).unwrap().0.unwrap();
13212
13213        let mut frontend = FrontendState::new();
13214
13215        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13216        let mock_ctx = ExecutorContext::new_mock(None).await;
13217        let version = Version(0);
13218
13219        frontend.hack_set_program(&ctx, program).await.unwrap();
13220        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13221        let sketch_id = sketch_object.id;
13222        let sketch = expect_sketch(sketch_object);
13223        let line1_id = *sketch.segments.get(2).unwrap();
13224        let line2_id = *sketch.segments.get(5).unwrap();
13225
13226        let constraint = Constraint::Parallel(Parallel {
13227            lines: vec![line1_id, line2_id],
13228        });
13229        let (src_delta, scene_delta) = frontend
13230            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13231            .await
13232            .unwrap();
13233        assert_eq!(
13234            src_delta.text.as_str(),
13235            "\
13236sketch(on = XY) {
13237  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13238  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13239  parallel([line1, line2])
13240}
13241"
13242        );
13243        assert_eq!(
13244            scene_delta.new_graph.objects.len(),
13245            9,
13246            "{:#?}",
13247            scene_delta.new_graph.objects
13248        );
13249
13250        ctx.close().await;
13251        mock_ctx.close().await;
13252    }
13253
13254    #[tokio::test(flavor = "multi_thread")]
13255    async fn test_lines_parallel_multiline() {
13256        let initial_source = "\
13257sketch(on = XY) {
13258  line(start = [var 1, var 2], end = [var 3, var 4])
13259  line(start = [var 5, var 6], end = [var 7, var 8])
13260  line(start = [var 9, var 10], end = [var 11, var 12])
13261}
13262";
13263
13264        let program = Program::parse(initial_source).unwrap().0.unwrap();
13265
13266        let mut frontend = FrontendState::new();
13267
13268        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13269        let mock_ctx = ExecutorContext::new_mock(None).await;
13270        let version = Version(0);
13271
13272        frontend.hack_set_program(&ctx, program).await.unwrap();
13273        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13274        let sketch_id = sketch_object.id;
13275        let sketch = expect_sketch(sketch_object);
13276        let line1_id = *sketch.segments.get(2).unwrap();
13277        let line2_id = *sketch.segments.get(5).unwrap();
13278        let line3_id = *sketch.segments.get(8).unwrap();
13279
13280        let constraint = Constraint::Parallel(Parallel {
13281            lines: vec![line1_id, line2_id, line3_id],
13282        });
13283        let (src_delta, scene_delta) = frontend
13284            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13285            .await
13286            .unwrap();
13287        assert_eq!(
13288            src_delta.text.as_str(),
13289            "\
13290sketch(on = XY) {
13291  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13292  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13293  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13294  parallel([line1, line2, line3])
13295}
13296"
13297        );
13298
13299        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13300        let sketch = expect_sketch(sketch_object);
13301        assert_eq!(sketch.constraints.len(), 1);
13302
13303        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13304        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13305            panic!("Expected constraint object");
13306        };
13307        let Constraint::Parallel(parallel) = constraint else {
13308            panic!("Expected parallel constraint");
13309        };
13310        assert_eq!(parallel.lines.len(), 3);
13311
13312        ctx.close().await;
13313        mock_ctx.close().await;
13314    }
13315
13316    #[tokio::test(flavor = "multi_thread")]
13317    async fn test_lines_perpendicular() {
13318        let initial_source = "\
13319sketch(on = XY) {
13320  line(start = [var 1, var 2], end = [var 3, var 4])
13321  line(start = [var 5, var 6], end = [var 7, var 8])
13322}
13323";
13324
13325        let program = Program::parse(initial_source).unwrap().0.unwrap();
13326
13327        let mut frontend = FrontendState::new();
13328
13329        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13330        let mock_ctx = ExecutorContext::new_mock(None).await;
13331        let version = Version(0);
13332
13333        frontend.hack_set_program(&ctx, program).await.unwrap();
13334        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13335        let sketch_id = sketch_object.id;
13336        let sketch = expect_sketch(sketch_object);
13337        let line1_id = *sketch.segments.get(2).unwrap();
13338        let line2_id = *sketch.segments.get(5).unwrap();
13339
13340        let constraint = Constraint::Perpendicular(Perpendicular {
13341            lines: vec![line1_id, line2_id],
13342        });
13343        let (src_delta, scene_delta) = frontend
13344            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13345            .await
13346            .unwrap();
13347        assert_eq!(
13348            src_delta.text.as_str(),
13349            "\
13350sketch(on = XY) {
13351  line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13352  line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13353  perpendicular([line1, line2])
13354}
13355"
13356        );
13357        assert_eq!(
13358            scene_delta.new_graph.objects.len(),
13359            9,
13360            "{:#?}",
13361            scene_delta.new_graph.objects
13362        );
13363
13364        ctx.close().await;
13365        mock_ctx.close().await;
13366    }
13367
13368    #[tokio::test(flavor = "multi_thread")]
13369    async fn test_lines_angle() {
13370        let initial_source = "\
13371sketch(on = XY) {
13372  line(start = [var 1, var 2], end = [var 3, var 4])
13373  line(start = [var 5, var 6], end = [var 7, var 8])
13374}
13375";
13376
13377        let program = Program::parse(initial_source).unwrap().0.unwrap();
13378
13379        let mut frontend = FrontendState::new();
13380
13381        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13382        let mock_ctx = ExecutorContext::new_mock(None).await;
13383        let version = Version(0);
13384
13385        frontend.hack_set_program(&ctx, program).await.unwrap();
13386        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13387        let sketch_id = sketch_object.id;
13388        let sketch = expect_sketch(sketch_object);
13389        let line1_id = *sketch.segments.get(2).unwrap();
13390        let line2_id = *sketch.segments.get(5).unwrap();
13391
13392        let constraint = Constraint::Angle(Angle {
13393            lines: vec![line1_id, line2_id],
13394            angle: Number {
13395                value: 30.0,
13396                units: NumericSuffix::Deg,
13397            },
13398            source: Default::default(),
13399        });
13400        let (src_delta, scene_delta) = frontend
13401            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13402            .await
13403            .unwrap();
13404        assert_eq!(
13405            src_delta.text.as_str(),
13406            // The lack indentation is a formatter bug.
13407            "\
13408sketch(on = XY) {
13409  line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13410  line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13411  angle([line1, line2]) == 30deg
13412}
13413"
13414        );
13415        assert_eq!(
13416            scene_delta.new_graph.objects.len(),
13417            9,
13418            "{:#?}",
13419            scene_delta.new_graph.objects
13420        );
13421
13422        ctx.close().await;
13423        mock_ctx.close().await;
13424    }
13425
13426    #[tokio::test(flavor = "multi_thread")]
13427    async fn test_segments_tangent() {
13428        let initial_source = "\
13429sketch(on = XY) {
13430  line(start = [var 1, var 2], end = [var 3, var 4])
13431  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13432}
13433";
13434
13435        let program = Program::parse(initial_source).unwrap().0.unwrap();
13436
13437        let mut frontend = FrontendState::new();
13438
13439        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13440        let mock_ctx = ExecutorContext::new_mock(None).await;
13441        let version = Version(0);
13442
13443        frontend.hack_set_program(&ctx, program).await.unwrap();
13444        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13445        let sketch_id = sketch_object.id;
13446        let sketch = expect_sketch(sketch_object);
13447        let line1_id = *sketch.segments.get(2).unwrap();
13448        let arc1_id = *sketch.segments.get(6).unwrap();
13449
13450        let constraint = Constraint::Tangent(Tangent {
13451            input: vec![line1_id, arc1_id],
13452        });
13453        let (src_delta, scene_delta) = frontend
13454            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13455            .await
13456            .unwrap();
13457        assert_eq!(
13458            src_delta.text.as_str(),
13459            "\
13460sketch(on = XY) {
13461  line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13462  arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13463  tangent([line1, arc1])
13464}
13465"
13466        );
13467        assert_eq!(
13468            scene_delta.new_graph.objects.len(),
13469            10,
13470            "{:#?}",
13471            scene_delta.new_graph.objects
13472        );
13473
13474        ctx.close().await;
13475        mock_ctx.close().await;
13476    }
13477
13478    #[tokio::test(flavor = "multi_thread")]
13479    async fn test_point_midpoint() {
13480        let initial_source = "\
13481sketch(on = XY) {
13482  point(at = [var 1, var 1])
13483  line(start = [var 0, var 0], end = [var 6, var 4])
13484}
13485";
13486
13487        let program = Program::parse(initial_source).unwrap().0.unwrap();
13488
13489        let mut frontend = FrontendState::new();
13490
13491        let ctx = ExecutorContext::new_mock(None).await;
13492        let version = Version(0);
13493
13494        frontend.program = program.clone();
13495        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13496        frontend.update_state_after_exec(outcome, true);
13497        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13498        let sketch_id = sketch_object.id;
13499        let sketch = expect_sketch(sketch_object);
13500        let point_id = *sketch.segments.first().unwrap();
13501        let line_id = *sketch.segments.get(3).unwrap();
13502
13503        let constraint = Constraint::Midpoint(Midpoint {
13504            point: ConstraintSegment::from(point_id),
13505            segment: line_id,
13506        });
13507        let (src_delta, scene_delta) = frontend
13508            .add_constraint(&ctx, version, sketch_id, constraint)
13509            .await
13510            .unwrap();
13511        assert_eq!(
13512            src_delta.text.as_str(),
13513            "\
13514sketch(on = XY) {
13515  point1 = point(at = [var 2.33, var 1.67])
13516  line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13517  midpoint(line1, point = point1)
13518}
13519"
13520        );
13521        assert_eq!(
13522            scene_delta.new_graph.objects.len(),
13523            7,
13524            "{:#?}",
13525            scene_delta.new_graph.objects
13526        );
13527
13528        ctx.close().await;
13529    }
13530
13531    #[tokio::test(flavor = "multi_thread")]
13532    async fn test_segments_symmetric() {
13533        let initial_source = "\
13534sketch(on = XY) {
13535  line(start = [var 0, var 0], end = [var 0, var 4])
13536  line(start = [var 4, var 0], end = [var 4, var 4])
13537  line(start = [var 2, var -1], end = [var 2, var 5])
13538}
13539";
13540
13541        let program = Program::parse(initial_source).unwrap().0.unwrap();
13542
13543        let mut frontend = FrontendState::new();
13544
13545        let ctx = ExecutorContext::new_mock(None).await;
13546        let version = Version(0);
13547
13548        frontend.program = program.clone();
13549        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13550        frontend.update_state_after_exec(outcome, true);
13551        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13552        let sketch_id = sketch_object.id;
13553        let sketch = expect_sketch(sketch_object);
13554        let line1_id = *sketch.segments.get(2).unwrap();
13555        let line2_id = *sketch.segments.get(5).unwrap();
13556        let axis_id = *sketch.segments.get(8).unwrap();
13557
13558        let constraint = Constraint::Symmetric(Symmetric {
13559            input: vec![line1_id, line2_id],
13560            axis: axis_id,
13561        });
13562        let (src_delta, scene_delta) = frontend
13563            .add_constraint(&ctx, version, sketch_id, constraint)
13564            .await
13565            .unwrap();
13566        assert_eq!(
13567            src_delta.text.as_str(),
13568            "\
13569sketch(on = XY) {
13570  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13571  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13572  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13573  symmetric([line1, line2], axis = line3)
13574}
13575"
13576        );
13577        assert_eq!(
13578            scene_delta.new_graph.objects.len(),
13579            12,
13580            "{:#?}",
13581            scene_delta.new_graph.objects
13582        );
13583
13584        ctx.close().await;
13585    }
13586
13587    #[tokio::test(flavor = "multi_thread")]
13588    async fn test_point_arc_midpoint() {
13589        let initial_source = "\
13590sketch(on = XY) {
13591  point(at = [var 6, var 3])
13592  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13593}
13594";
13595
13596        let program = Program::parse(initial_source).unwrap().0.unwrap();
13597
13598        let mut frontend = FrontendState::new();
13599
13600        let ctx = ExecutorContext::new_mock(None).await;
13601        let version = Version(0);
13602
13603        frontend.program = program.clone();
13604        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13605        frontend.update_state_after_exec(outcome, true);
13606        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13607        let sketch_id = sketch_object.id;
13608        let sketch = expect_sketch(sketch_object);
13609        let point_id = *sketch.segments.first().unwrap();
13610        let arc_id = *sketch.segments.get(4).unwrap();
13611
13612        let constraint = Constraint::Midpoint(Midpoint {
13613            point: ConstraintSegment::from(point_id),
13614            segment: arc_id,
13615        });
13616        let (src_delta, scene_delta) = frontend
13617            .add_constraint(&ctx, version, sketch_id, constraint)
13618            .await
13619            .unwrap();
13620        assert_eq!(
13621            src_delta.text.as_str(),
13622            "\
13623sketch(on = XY) {
13624  point1 = point(at = [var 6, var 2.35])
13625  arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13626  midpoint(arc1, point = point1)
13627}
13628"
13629        );
13630        assert_eq!(
13631            scene_delta.new_graph.objects.len(),
13632            8,
13633            "{:#?}",
13634            scene_delta.new_graph.objects
13635        );
13636
13637        ctx.close().await;
13638    }
13639
13640    #[tokio::test(flavor = "multi_thread")]
13641    async fn test_origin_line_midpoint() {
13642        let initial_source = "\
13643sketch(on = XY) {
13644  line(start = [var 0, var 0], end = [var 6, var 4])
13645}
13646";
13647
13648        let program = Program::parse(initial_source).unwrap().0.unwrap();
13649
13650        let mut frontend = FrontendState::new();
13651
13652        let ctx = ExecutorContext::new_mock(None).await;
13653        let version = Version(0);
13654
13655        frontend.program = program.clone();
13656        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13657        frontend.update_state_after_exec(outcome, true);
13658        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13659        let sketch_id = sketch_object.id;
13660        let sketch = expect_sketch(sketch_object);
13661        let line_id = *sketch.segments.get(2).unwrap();
13662
13663        let constraint = Constraint::Midpoint(Midpoint {
13664            point: ConstraintSegment::ORIGIN,
13665            segment: line_id,
13666        });
13667        let (src_delta, scene_delta) = frontend
13668            .add_constraint(&ctx, version, sketch_id, constraint)
13669            .await
13670            .unwrap();
13671        assert_eq!(
13672            src_delta.text.as_str(),
13673            "\
13674sketch(on = XY) {
13675  line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13676  midpoint(line1, point = ORIGIN)
13677}
13678"
13679        );
13680        assert_eq!(
13681            scene_delta.new_graph.objects.len(),
13682            6,
13683            "{:#?}",
13684            scene_delta.new_graph.objects
13685        );
13686
13687        ctx.close().await;
13688    }
13689
13690    #[tokio::test(flavor = "multi_thread")]
13691    async fn test_origin_arc_midpoint() {
13692        let initial_source = "\
13693sketch(on = XY) {
13694  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13695}
13696";
13697
13698        let program = Program::parse(initial_source).unwrap().0.unwrap();
13699
13700        let mut frontend = FrontendState::new();
13701
13702        let ctx = ExecutorContext::new_mock(None).await;
13703        let version = Version(0);
13704
13705        frontend.program = program.clone();
13706        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13707        frontend.update_state_after_exec(outcome, true);
13708        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13709        let sketch_id = sketch_object.id;
13710        let sketch = expect_sketch(sketch_object);
13711        let arc_id = *sketch.segments.get(3).unwrap();
13712
13713        let constraint = Constraint::Midpoint(Midpoint {
13714            point: ConstraintSegment::ORIGIN,
13715            segment: arc_id,
13716        });
13717        let (src_delta, scene_delta) = frontend
13718            .add_constraint(&ctx, version, sketch_id, constraint)
13719            .await
13720            .unwrap();
13721        assert_eq!(
13722            src_delta.text.as_str(),
13723            "\
13724sketch(on = XY) {
13725  arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13726  midpoint(arc1, point = ORIGIN)
13727}
13728"
13729        );
13730        assert_eq!(
13731            scene_delta.new_graph.objects.len(),
13732            7,
13733            "{:#?}",
13734            scene_delta.new_graph.objects
13735        );
13736
13737        ctx.close().await;
13738    }
13739
13740    #[tokio::test(flavor = "multi_thread")]
13741    async fn test_segments_symmetric_arcs() {
13742        let initial_source = "\
13743sketch(on = XY) {
13744  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13745  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13746  line(start = [var 0, var -10], end = [var 0, var 10])
13747}
13748";
13749
13750        let program = Program::parse(initial_source).unwrap().0.unwrap();
13751
13752        let mut frontend = FrontendState::new();
13753
13754        let ctx = ExecutorContext::new_mock(None).await;
13755        let version = Version(0);
13756
13757        frontend.program = program.clone();
13758        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13759        frontend.update_state_after_exec(outcome, true);
13760        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13761        let sketch_id = sketch_object.id;
13762        let sketch = expect_sketch(sketch_object);
13763        let arc1_id = *sketch.segments.get(3).unwrap();
13764        let arc2_id = *sketch.segments.get(7).unwrap();
13765        let axis_id = *sketch.segments.get(10).unwrap();
13766
13767        let constraint = Constraint::Symmetric(Symmetric {
13768            input: vec![arc1_id, arc2_id],
13769            axis: axis_id,
13770        });
13771        let (src_delta, scene_delta) = frontend
13772            .add_constraint(&ctx, version, sketch_id, constraint)
13773            .await
13774            .unwrap();
13775        assert_eq!(
13776            src_delta.text.as_str(),
13777            "\
13778sketch(on = XY) {
13779  arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13780  arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13781  line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13782  symmetric([arc1, arc2], axis = line1)
13783}
13784"
13785        );
13786        assert_eq!(
13787            scene_delta.new_graph.objects.len(),
13788            14,
13789            "{:#?}",
13790            scene_delta.new_graph.objects
13791        );
13792
13793        ctx.close().await;
13794    }
13795
13796    #[tokio::test(flavor = "multi_thread")]
13797    async fn test_sketch_on_face_simple() {
13798        let initial_source = "\
13799len = 2mm
13800cube = startSketchOn(XY)
13801  |> startProfile(at = [0, 0])
13802  |> line(end = [len, 0], tag = $side)
13803  |> line(end = [0, len])
13804  |> line(end = [-len, 0])
13805  |> line(end = [0, -len])
13806  |> close()
13807  |> extrude(length = len)
13808
13809face = faceOf(cube, face = side)
13810";
13811
13812        let program = Program::parse(initial_source).unwrap().0.unwrap();
13813
13814        let mut frontend = FrontendState::new();
13815
13816        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13817        let mock_ctx = ExecutorContext::new_mock(None).await;
13818        let version = Version(0);
13819
13820        frontend.hack_set_program(&ctx, program).await.unwrap();
13821        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13822        let face_id = face_object.id;
13823
13824        let sketch_args = SketchCtor {
13825            on: Plane::Object(face_id),
13826        };
13827        let (_src_delta, scene_delta, sketch_id) = frontend
13828            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13829            .await
13830            .unwrap();
13831        assert_eq!(sketch_id, ObjectId(2));
13832        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13833        let sketch_object = &scene_delta.new_graph.objects[2];
13834        assert_eq!(sketch_object.id, ObjectId(2));
13835        assert_eq!(
13836            sketch_object.kind,
13837            ObjectKind::Sketch(Sketch {
13838                args: SketchCtor {
13839                    on: Plane::Object(face_id),
13840                },
13841                plane: face_id,
13842                segments: vec![],
13843                constraints: vec![],
13844            })
13845        );
13846        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13847
13848        ctx.close().await;
13849        mock_ctx.close().await;
13850    }
13851
13852    #[tokio::test(flavor = "multi_thread")]
13853    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13854        let initial_source = "\
13855s = sketch(on = YZ) {
13856  line1 = line(start = [0, 0], end = [0, 1])
13857  line2 = line(start = [0, 1], end = [1, 1])
13858  line3 = line(start = [1, 1], end = [0, 0])
13859}
13860region001 = region(point = [0.1, 0.1], sketch = s)
13861extrude001 = extrude(region001, length = 5)
13862";
13863
13864        let program = Program::parse(initial_source).unwrap().0.unwrap();
13865
13866        let mut frontend = FrontendState::new();
13867        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13868        let version = Version(0);
13869
13870        frontend.hack_set_program(&ctx, program).await.unwrap();
13871        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13872
13873        let sketch_args = SketchCtor {
13874            on: Plane::Object(wall_object_id),
13875        };
13876        let (src_delta, _scene_delta, _sketch_id) = frontend
13877            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13878            .await
13879            .unwrap();
13880        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13881
13882        ctx.close().await;
13883    }
13884
13885    #[tokio::test(flavor = "multi_thread")]
13886    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13887        let initial_source = "\
13888sketch001 = sketch(on = YZ) {
13889  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13890  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13891  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13892  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13893  coincident([line1.end, line2.start])
13894  coincident([line2.end, line3.start])
13895  coincident([line3.end, line4.start])
13896  coincident([line4.end, line1.start])
13897  parallel([line2, line4])
13898  parallel([line3, line1])
13899  perpendicular([line1, line2])
13900  horizontal(line3)
13901  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13902}
13903region001 = region(point = [3.1, 3.74], sketch = sketch001)
13904extrude001 = extrude(region001, length = 5)
13905";
13906
13907        let program = Program::parse(initial_source).unwrap().0.unwrap();
13908
13909        let mut frontend = FrontendState::new();
13910        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13911        let version = Version(0);
13912
13913        frontend.hack_set_program(&ctx, program).await.unwrap();
13914        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13915
13916        let sketch_args = SketchCtor {
13917            on: Plane::Object(wall_object_id),
13918        };
13919        let (src_delta, _scene_delta, _sketch_id) = frontend
13920            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13921            .await
13922            .unwrap();
13923        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13924
13925        ctx.close().await;
13926    }
13927
13928    #[tokio::test(flavor = "multi_thread")]
13929    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13930        let initial_source = "\
13931@settings(kclVersion = 2.0)
13932
13933sketch001 = sketch(on = XY) {
13934  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13935  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13936}
13937hidden001 = hide(sketch001)
13938region001 = region(segments = [sketch001.circle2])
13939region002 = region(segments = [sketch001.circle1])
13940extrude001 = extrude([region001, region002], length = 5)
13941";
13942
13943        let program = Program::parse(initial_source).unwrap().0.unwrap();
13944        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13945        let version = Version(0);
13946
13947        for (solid_output_index, expected_face) in [
13948            (0, "faceOf(extrude001[0], face = END)"),
13949            (1, "faceOf(extrude001[1], face = END)"),
13950        ] {
13951            let mut frontend = FrontendState::new();
13952            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13953            let cap_object_id = find_cap_object_id_with_solid_output_index(
13954                &frontend.scene_graph,
13955                crate::frontend::api::CapKind::End,
13956                solid_output_index,
13957            )
13958            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13959
13960            let sketch_args = SketchCtor {
13961                on: Plane::Object(cap_object_id),
13962            };
13963            let (src_delta, _scene_delta, _sketch_id) = frontend
13964                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13965                .await
13966                .unwrap();
13967
13968            assert!(
13969                src_delta.text.contains(expected_face),
13970                "expected `{expected_face}` in:\n{}",
13971                src_delta.text
13972            );
13973            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13974        }
13975
13976        ctx.close().await;
13977    }
13978
13979    #[tokio::test(flavor = "multi_thread")]
13980    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13981        let initial_source = "\
13982@settings(kclVersion = 2.0)
13983
13984sketch001 = sketch(on = XY) {
13985  rect1Line1 = line(start = [0, 0], end = [1, 0])
13986  rect1Line2 = line(start = [1, 0], end = [1, 1])
13987  rect1Line3 = line(start = [1, 1], end = [0, 1])
13988  rect1Line4 = line(start = [0, 1], end = [0, 0])
13989  rect2Line1 = line(start = [3, 0], end = [4, 0])
13990  rect2Line2 = line(start = [4, 0], end = [4, 1])
13991  rect2Line3 = line(start = [4, 1], end = [3, 1])
13992  rect2Line4 = line(start = [3, 1], end = [3, 0])
13993}
13994hidden001 = hide(sketch001)
13995region001 = region(segments = [
13996  sketch001.rect1Line4,
13997  sketch001.rect1Line1
13998])
13999region002 = region(segments = [
14000  sketch001.rect2Line4,
14001  sketch001.rect2Line1
14002])
14003extrude001 = extrude([region001, region002], length = 5)
14004";
14005
14006        let program = Program::parse(initial_source).unwrap().0.unwrap();
14007        let mut frontend = FrontendState::new();
14008        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14009        let version = Version(0);
14010
14011        frontend.hack_set_program(&ctx, program).await.unwrap();
14012        let region_call = "\
14013region(segments = [
14014  sketch001.rect1Line4,
14015  sketch001.rect1Line1
14016])";
14017        let region_call_start = initial_source.find(region_call).unwrap();
14018        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14019        let segment_call = "line(start = [0, 0], end = [1, 0])";
14020        let segment_call_start = initial_source.find(segment_call).unwrap();
14021        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14022        let wall_object_id = frontend
14023            .scene_graph
14024            .objects
14025            .iter()
14026            .find_map(|object| match &object.kind {
14027                ObjectKind::Wall(wall)
14028                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14029                        && wall.source.segment.range == segment_range =>
14030                {
14031                    Some(object.id)
14032                }
14033                _ => None,
14034            })
14035            .expect("expected a wall object for region001.tags.rect1Line1");
14036
14037        let sketch_args = SketchCtor {
14038            on: Plane::Object(wall_object_id),
14039        };
14040        let (src_delta, _scene_delta, _sketch_id) = frontend
14041            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14042            .await
14043            .unwrap();
14044
14045        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14046        assert!(
14047            src_delta.text.contains(expected_face),
14048            "expected `{expected_face}` in:\n{}",
14049            src_delta.text
14050        );
14051        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14052
14053        ctx.close().await;
14054    }
14055
14056    #[test]
14057    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14058        let source = "\
14059sketch001 = sketch(on = XY) {
14060  line(start = [0, 0], end = [1, 0])
14061}
14062part = subtract(boxSolid, tools = [cutSolid])
14063  |> appearance(color = \"#8f96a3\")
14064";
14065        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14066        let line_start = source.find("line").unwrap();
14067        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14068        let line_ref = SourceRef::Simple {
14069            range: [line_start, line_end, 0].into(),
14070            node_path: None,
14071        };
14072        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14073
14074        let subtract_start = source.find("subtract").unwrap();
14075        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14076        let subtract_ref = SourceRef::Simple {
14077            range: [subtract_start, subtract_end, 0].into(),
14078            node_path: None,
14079        };
14080        assert_eq!(
14081            variable_name_containing_source_ref(&ast, &subtract_ref),
14082            Some("part".to_owned())
14083        );
14084    }
14085
14086    #[tokio::test(flavor = "multi_thread")]
14087    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14088        clear_mem_cache().await;
14089        let source = "\
14090boxSolid = startSketchOn(XY)
14091  |> startProfile(at = [0, 0])
14092  |> line(end = [4, 0], tag = $bottomEdge)
14093  |> line(end = [0, 4])
14094  |> line(end = [-4, 0])
14095  |> close()
14096  |> extrude(length = 10)
14097cutSolid = startSketchOn(XY)
14098  |> startProfile(at = [1, 1])
14099  |> line(end = [1, 0])
14100  |> line(end = [0, 1])
14101  |> line(end = [-1, 0])
14102  |> close()
14103  |> extrude(length = 10)
14104part = subtract(boxSolid, tools = [cutSolid])
14105  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14106";
14107        let program = Program::parse(source).unwrap().0.unwrap();
14108        let mut frontend = FrontendState::new();
14109        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14110        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14111            SetProgramOutcome::Success { .. } => {}
14112            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14113        }
14114
14115        let sweep_call_start = source.find("extrude").unwrap();
14116        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14117        let part_call_start = source.find("subtract").unwrap();
14118        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14119        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14120        let composite_range = [part_call_start, part_call_end, 0].into();
14121
14122        let cap_object = frontend
14123            .scene_graph
14124            .objects
14125            .iter()
14126            .find(|object| {
14127                matches!(
14128                    &object.kind,
14129                    ObjectKind::Cap(crate::frontend::api::Cap {
14130                        kind: crate::frontend::api::CapKind::End,
14131                        source,
14132                        ..
14133                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14134                )
14135            })
14136            .expect("expected end cap object to trace through subtract and original extrude");
14137
14138        let mut ast = frontend.program.ast.clone();
14139        let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
14140        let cap_face_decl = ast::VariableDeclaration::new(
14141            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14142            ast::ItemVisibility::Default,
14143            ast::VariableKind::Const,
14144        );
14145        ast.body
14146            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
14147                cap_face_decl,
14148            ))));
14149        let generated_source = source_from_ast(&ast);
14150
14151        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14152        assert!(!generated_source.contains("faceOf(boxSolid"));
14153        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14154            panic!("expected faceOf call");
14155        };
14156        assert_eq!(call.callee.name.name, "faceOf");
14157        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14158            panic!("expected solid name");
14159        };
14160        assert_eq!(solid_name.name.name, "part");
14161        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14162            panic!("expected face name");
14163        };
14164        assert_eq!(face_name.name.name, "END");
14165
14166        ctx.close().await;
14167    }
14168
14169    #[tokio::test(flavor = "multi_thread")]
14170    async fn test_sketch_on_plane_incremental() {
14171        let initial_source = "\
14172len = 2mm
14173cube = startSketchOn(XY)
14174  |> startProfile(at = [0, 0])
14175  |> line(end = [len, 0], tag = $side)
14176  |> line(end = [0, len])
14177  |> line(end = [-len, 0])
14178  |> line(end = [0, -len])
14179  |> close()
14180  |> extrude(length = len)
14181
14182plane = planeOf(cube, face = side)
14183";
14184
14185        let program = Program::parse(initial_source).unwrap().0.unwrap();
14186
14187        let mut frontend = FrontendState::new();
14188
14189        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14190        let mock_ctx = ExecutorContext::new_mock(None).await;
14191        let version = Version(0);
14192
14193        frontend.hack_set_program(&ctx, program).await.unwrap();
14194        // Find the last plane since the first plane is the XY plane.
14195        let plane_object = frontend
14196            .scene_graph
14197            .objects
14198            .iter()
14199            .rev()
14200            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14201            .unwrap();
14202        let plane_id = plane_object.id;
14203
14204        let sketch_args = SketchCtor {
14205            on: Plane::Object(plane_id),
14206        };
14207        let (src_delta, scene_delta, sketch_id) = frontend
14208            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14209            .await
14210            .unwrap();
14211        assert_eq!(
14212            src_delta.text.as_str(),
14213            "\
14214len = 2mm
14215cube = startSketchOn(XY)
14216  |> startProfile(at = [0, 0])
14217  |> line(end = [len, 0], tag = $side)
14218  |> line(end = [0, len])
14219  |> line(end = [-len, 0])
14220  |> line(end = [0, -len])
14221  |> close()
14222  |> extrude(length = len)
14223
14224plane = planeOf(cube, face = side)
14225sketch001 = sketch(on = plane) {
14226}
14227"
14228        );
14229        assert_eq!(sketch_id, ObjectId(2));
14230        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14231        let sketch_object = &scene_delta.new_graph.objects[2];
14232        assert_eq!(sketch_object.id, ObjectId(2));
14233        assert_eq!(
14234            sketch_object.kind,
14235            ObjectKind::Sketch(Sketch {
14236                args: SketchCtor {
14237                    on: Plane::Object(plane_id),
14238                },
14239                plane: plane_id,
14240                segments: vec![],
14241                constraints: vec![],
14242            })
14243        );
14244        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14245
14246        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14247        assert_eq!(plane_object.id, plane_id);
14248        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14249
14250        ctx.close().await;
14251        mock_ctx.close().await;
14252    }
14253
14254    #[tokio::test(flavor = "multi_thread")]
14255    async fn test_new_sketch_uses_unique_variable_name() {
14256        let initial_source = "\
14257sketch1 = sketch(on = XY) {
14258}
14259";
14260
14261        let program = Program::parse(initial_source).unwrap().0.unwrap();
14262
14263        let mut frontend = FrontendState::new();
14264        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14265        let version = Version(0);
14266
14267        frontend.hack_set_program(&ctx, program).await.unwrap();
14268
14269        let sketch_args = SketchCtor {
14270            on: Plane::Default(PlaneName::Yz),
14271        };
14272        let (src_delta, _, _) = frontend
14273            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14274            .await
14275            .unwrap();
14276
14277        assert_eq!(
14278            src_delta.text.as_str(),
14279            "\
14280sketch1 = sketch(on = XY) {
14281}
14282sketch001 = sketch(on = YZ) {
14283}
14284"
14285        );
14286
14287        ctx.close().await;
14288    }
14289
14290    #[tokio::test(flavor = "multi_thread")]
14291    async fn test_new_sketch_twice_using_same_plane() {
14292        let initial_source = "\
14293sketch1 = sketch(on = XY) {
14294}
14295";
14296
14297        let program = Program::parse(initial_source).unwrap().0.unwrap();
14298
14299        let mut frontend = FrontendState::new();
14300        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14301        let version = Version(0);
14302
14303        frontend.hack_set_program(&ctx, program).await.unwrap();
14304
14305        let sketch_args = SketchCtor {
14306            on: Plane::Default(PlaneName::Xy),
14307        };
14308        let (src_delta, _, _) = frontend
14309            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14310            .await
14311            .unwrap();
14312
14313        assert_eq!(
14314            src_delta.text.as_str(),
14315            "\
14316sketch1 = sketch(on = XY) {
14317}
14318sketch001 = sketch(on = XY) {
14319}
14320"
14321        );
14322
14323        ctx.close().await;
14324    }
14325
14326    #[tokio::test(flavor = "multi_thread")]
14327    async fn test_sketch_mode_reuses_cached_on_expression() {
14328        let initial_source = "\
14329width = 2mm
14330sketch(on = offsetPlane(XY, offset = width)) {
14331  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14332  distance([line1.start, line1.end]) == width
14333}
14334";
14335        let program = Program::parse(initial_source).unwrap().0.unwrap();
14336
14337        let mut frontend = FrontendState::new();
14338        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14339        let mock_ctx = ExecutorContext::new_mock(None).await;
14340        let version = Version(0);
14341        let project_id = ProjectId(0);
14342        let file_id = FileId(0);
14343
14344        frontend.hack_set_program(&ctx, program).await.unwrap();
14345        let initial_object_count = frontend.scene_graph.objects.len();
14346        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14347            .expect("Expected sketch object to exist")
14348            .id;
14349
14350        // Entering sketch mode should reuse cached `on` expression state
14351        // (offsetPlane result), not fail or create extra on-surface objects.
14352        let scene_delta = frontend
14353            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14354            .await
14355            .unwrap();
14356        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14357
14358        // A follow-up sketch-mode execution should keep the same stable object
14359        // graph shape as well.
14360        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14361        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14362
14363        ctx.close().await;
14364        mock_ctx.close().await;
14365    }
14366
14367    #[tokio::test(flavor = "multi_thread")]
14368    async fn test_multiple_sketch_blocks() {
14369        let initial_source = "\
14370// Cube that requires the engine.
14371width = 2
14372sketch001 = startSketchOn(XY)
14373profile001 = startProfile(sketch001, at = [0, 0])
14374  |> yLine(length = width, tag = $seg1)
14375  |> xLine(length = width)
14376  |> yLine(length = -width)
14377  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14378  |> close()
14379extrude001 = extrude(profile001, length = width)
14380
14381// Get a value that requires the engine.
14382x = segLen(seg1)
14383
14384// Triangle with side length 2*x.
14385sketch(on = XY) {
14386  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14387  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14388  coincident([line1.end, line2.start])
14389  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14390  coincident([line2.end, line3.start])
14391  coincident([line3.end, line1.start])
14392  equalLength([line3, line1])
14393  equalLength([line1, line2])
14394  distance([line1.start, line1.end]) == 2*x
14395}
14396
14397// Line segment with length x.
14398sketch2 = sketch(on = XY) {
14399  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14400  distance([line1.start, line1.end]) == x
14401}
14402";
14403
14404        let program = Program::parse(initial_source).unwrap().0.unwrap();
14405
14406        let mut frontend = FrontendState::new();
14407
14408        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14409        let mock_ctx = ExecutorContext::new_mock(None).await;
14410        let version = Version(0);
14411        let project_id = ProjectId(0);
14412        let file_id = FileId(0);
14413
14414        frontend.hack_set_program(&ctx, program).await.unwrap();
14415        let sketch_objects = frontend
14416            .scene_graph
14417            .objects
14418            .iter()
14419            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14420            .collect::<Vec<_>>();
14421        let sketch1_id = sketch_objects.first().unwrap().id;
14422        let sketch2_id = sketch_objects.get(1).unwrap().id;
14423        // First point in sketch1.
14424        let point1_id = ObjectId(sketch1_id.0 + 1);
14425        // First point in sketch2.
14426        let point2_id = ObjectId(sketch2_id.0 + 1);
14427
14428        // Edit the first sketch. Objects before the sketch block should be
14429        // present from execution cache so that we can sketch on prior planes,
14430        // for example. Objects after the first sketch block should not be
14431        // present since those statements are skipped in sketch mode.
14432        //
14433        // - startSketchOn(XY) Plane 1
14434        // - sketch on=XY Plane 1
14435        // - Sketch block 16
14436        let scene_delta = frontend
14437            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14438            .await
14439            .unwrap();
14440        assert_eq!(
14441            scene_delta.new_graph.objects.len(),
14442            18,
14443            "{:#?}",
14444            scene_delta.new_graph.objects
14445        );
14446
14447        // Edit a point in the first sketch.
14448        let point_ctor = PointCtor {
14449            position: Point2d {
14450                x: Expr::Var(Number {
14451                    value: 1.0,
14452                    units: NumericSuffix::Mm,
14453                }),
14454                y: Expr::Var(Number {
14455                    value: 2.0,
14456                    units: NumericSuffix::Mm,
14457                }),
14458            },
14459        };
14460        let segments = vec![ExistingSegmentCtor {
14461            id: point1_id,
14462            ctor: SegmentCtor::Point(point_ctor),
14463        }];
14464        let (src_delta, _) = frontend
14465            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14466            .await
14467            .unwrap();
14468        // Only the first sketch block changes.
14469        assert_eq!(
14470            src_delta.text.as_str(),
14471            "\
14472// Cube that requires the engine.
14473width = 2
14474sketch001 = startSketchOn(XY)
14475profile001 = startProfile(sketch001, at = [0, 0])
14476  |> yLine(length = width, tag = $seg1)
14477  |> xLine(length = width)
14478  |> yLine(length = -width)
14479  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14480  |> close()
14481extrude001 = extrude(profile001, length = width)
14482
14483// Get a value that requires the engine.
14484x = segLen(seg1)
14485
14486// Triangle with side length 2*x.
14487sketch(on = XY) {
14488  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14489  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14490  coincident([line1.end, line2.start])
14491  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14492  coincident([line2.end, line3.start])
14493  coincident([line3.end, line1.start])
14494  equalLength([line3, line1])
14495  equalLength([line1, line2])
14496  distance([line1.start, line1.end]) == 2 * x
14497}
14498
14499// Line segment with length x.
14500sketch2 = sketch(on = XY) {
14501  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14502  distance([line1.start, line1.end]) == x
14503}
14504"
14505        );
14506        let edited_sketch1_source = src_delta.text.clone();
14507
14508        // Execute mock to simulate drag end.
14509        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14510        assert_eq!(src_delta.text, edited_sketch1_source);
14511        // Exit sketch. Objects from the entire program should be present.
14512        //
14513        // - startSketchOn(XY) Plane 1
14514        // - sketch on=XY Plane 1
14515        // - Sketch block 16
14516        // - sketch on=XY cached
14517        // - Sketch block 5
14518        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14519        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14520
14521        // Edit the second sketch.
14522        //
14523        // - startSketchOn(XY) Plane 1
14524        // - sketch on=XY Plane 1
14525        // - Sketch block 16
14526        // - sketch on=XY cached
14527        // - Sketch block 5
14528        let scene_delta = frontend
14529            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14530            .await
14531            .unwrap();
14532        assert_eq!(
14533            scene_delta.new_graph.objects.len(),
14534            24,
14535            "{:#?}",
14536            scene_delta.new_graph.objects
14537        );
14538
14539        // Edit a point in the second sketch.
14540        let point_ctor = PointCtor {
14541            position: Point2d {
14542                x: Expr::Var(Number {
14543                    value: 3.0,
14544                    units: NumericSuffix::Mm,
14545                }),
14546                y: Expr::Var(Number {
14547                    value: 4.0,
14548                    units: NumericSuffix::Mm,
14549                }),
14550            },
14551        };
14552        let segments = vec![ExistingSegmentCtor {
14553            id: point2_id,
14554            ctor: SegmentCtor::Point(point_ctor),
14555        }];
14556        let (src_delta, _) = frontend
14557            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14558            .await
14559            .unwrap();
14560        // Only the second sketch block changes.
14561        assert_eq!(
14562            src_delta.text.as_str(),
14563            "\
14564// Cube that requires the engine.
14565width = 2
14566sketch001 = startSketchOn(XY)
14567profile001 = startProfile(sketch001, at = [0, 0])
14568  |> yLine(length = width, tag = $seg1)
14569  |> xLine(length = width)
14570  |> yLine(length = -width)
14571  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14572  |> close()
14573extrude001 = extrude(profile001, length = width)
14574
14575// Get a value that requires the engine.
14576x = segLen(seg1)
14577
14578// Triangle with side length 2*x.
14579sketch(on = XY) {
14580  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14581  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14582  coincident([line1.end, line2.start])
14583  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14584  coincident([line2.end, line3.start])
14585  coincident([line3.end, line1.start])
14586  equalLength([line3, line1])
14587  equalLength([line1, line2])
14588  distance([line1.start, line1.end]) == 2 * x
14589}
14590
14591// Line segment with length x.
14592sketch2 = sketch(on = XY) {
14593  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14594  distance([line1.start, line1.end]) == x
14595}
14596"
14597        );
14598        let edited_sketch2_source = src_delta.text.clone();
14599
14600        // Execute mock to simulate drag end.
14601        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14602        assert_eq!(src_delta.text, edited_sketch2_source);
14603
14604        ctx.close().await;
14605        mock_ctx.close().await;
14606    }
14607
14608    #[tokio::test(flavor = "multi_thread")]
14609    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14610        clear_mem_cache().await;
14611
14612        let source = r#"sketch001 = sketch(on = XZ) {
14613  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14614}
14615sketch002 = sketch(on = XY) {
14616  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14617  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14618  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14619  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14620  coincident([line1.end, line2.start])
14621  coincident([line2.end, line3.start])
14622  coincident([line3.end, line4.start])
14623  coincident([line4.end, line1.start])
14624  parallel([line2, line4])
14625  parallel([line3, line1])
14626  perpendicular([line1, line2])
14627  horizontal(line3)
14628  coincident([line1.start, ORIGIN])
14629}
14630"#;
14631
14632        let program = Program::parse(source).unwrap().0.unwrap();
14633        let mut frontend = FrontendState::new();
14634        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14635        let mock_ctx = ExecutorContext::new_mock(None).await;
14636        let version = Version(0);
14637        let project_id = ProjectId(0);
14638        let file_id = FileId(0);
14639
14640        frontend.hack_set_program(&ctx, program).await.unwrap();
14641        let sketch_objects = frontend
14642            .scene_graph
14643            .objects
14644            .iter()
14645            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14646            .collect::<Vec<_>>();
14647        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14648
14649        let sketch1_id = sketch_objects[0].id;
14650        let sketch2_id = sketch_objects[1].id;
14651
14652        frontend
14653            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14654            .await
14655            .unwrap();
14656        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14657
14658        let scene_delta = frontend
14659            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14660            .await
14661            .unwrap();
14662        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14663
14664        clear_mem_cache().await;
14665        ctx.close().await;
14666        mock_ctx.close().await;
14667    }
14668
14669    // Regression tests: operations on source code with extra whitespace/newlines.
14670    // These test that NodePath-based lookups work correctly when source ranges
14671    // are shifted by extra whitespace that wouldn't be present after formatting.
14672
14673    #[tokio::test(flavor = "multi_thread")]
14674    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14675        // Extra newlines after @settings line - this shifts all source ranges.
14676        let initial_source = "@settings(defaultLengthUnit = mm)
14677
14678
14679
14680sketch001 = sketch(on = XY) {
14681  point(at = [1in, 2in])
14682}
14683";
14684
14685        let program = Program::parse(initial_source).unwrap().0.unwrap();
14686        let mut frontend = FrontendState::new();
14687
14688        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14689        let mock_ctx = ExecutorContext::new_mock(None).await;
14690        let version = Version(0);
14691        let project_id = ProjectId(0);
14692        let file_id = FileId(0);
14693
14694        frontend.hack_set_program(&ctx, program).await.unwrap();
14695        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14696        let sketch_id = sketch_object.id;
14697
14698        // Edit sketch should succeed despite extra newlines.
14699        frontend
14700            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14701            .await
14702            .unwrap();
14703
14704        // Add a new point to the sketch.
14705        let point_ctor = PointCtor {
14706            position: Point2d {
14707                x: Expr::Number(Number {
14708                    value: 5.0,
14709                    units: NumericSuffix::Mm,
14710                }),
14711                y: Expr::Number(Number {
14712                    value: 6.0,
14713                    units: NumericSuffix::Mm,
14714                }),
14715            },
14716        };
14717        let segment = SegmentCtor::Point(point_ctor);
14718        let (src_delta, scene_delta) = frontend
14719            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14720            .await
14721            .unwrap();
14722        // After adding a point, the source should be reformatted with standard whitespace.
14723        assert!(
14724            src_delta.text.contains("point(at = [5mm, 6mm])"),
14725            "Expected new point in source, got: {}",
14726            src_delta.text
14727        );
14728        assert!(!scene_delta.new_objects.is_empty());
14729
14730        ctx.close().await;
14731        mock_ctx.close().await;
14732    }
14733
14734    #[tokio::test(flavor = "multi_thread")]
14735    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14736        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14737
14738        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14739        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14740
14741        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14742        assert!(
14743            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14744            "Expected experimental settings to be added to source"
14745        );
14746    }
14747
14748    #[tokio::test(flavor = "multi_thread")]
14749    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14750        // Extra newlines after @settings, with an empty sketch block.
14751        let initial_source = "@settings(defaultLengthUnit = mm)
14752
14753
14754
14755s = sketch(on = XY) {}
14756";
14757
14758        let program = Program::parse(initial_source).unwrap().0.unwrap();
14759        let mut frontend = FrontendState::new();
14760
14761        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14762        let mock_ctx = ExecutorContext::new_mock(None).await;
14763        let version = Version(0);
14764
14765        frontend.hack_set_program(&ctx, program).await.unwrap();
14766        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14767        let sketch_id = sketch_object.id;
14768
14769        let line_ctor = LineCtor {
14770            start: Point2d {
14771                x: Expr::Number(Number {
14772                    value: 0.0,
14773                    units: NumericSuffix::Mm,
14774                }),
14775                y: Expr::Number(Number {
14776                    value: 0.0,
14777                    units: NumericSuffix::Mm,
14778                }),
14779            },
14780            end: Point2d {
14781                x: Expr::Number(Number {
14782                    value: 10.0,
14783                    units: NumericSuffix::Mm,
14784                }),
14785                y: Expr::Number(Number {
14786                    value: 10.0,
14787                    units: NumericSuffix::Mm,
14788                }),
14789            },
14790            construction: None,
14791        };
14792        let segment = SegmentCtor::Line(line_ctor);
14793        let (src_delta, scene_delta) = frontend
14794            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14795            .await
14796            .unwrap();
14797        assert!(
14798            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14799            "Expected line in source, got: {}",
14800            src_delta.text
14801        );
14802        // Line creates start point, end point, and line segment.
14803        assert_eq!(scene_delta.new_objects.len(), 3);
14804
14805        ctx.close().await;
14806        mock_ctx.close().await;
14807    }
14808
14809    #[tokio::test(flavor = "multi_thread")]
14810    async fn test_extra_newlines_between_operations_edit_line() {
14811        // Extra newlines between @settings and sketch, and inside the sketch block.
14812        let initial_source = "@settings(defaultLengthUnit = mm)
14813
14814
14815sketch001 = sketch(on = XY) {
14816
14817  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14818
14819}
14820";
14821
14822        let program = Program::parse(initial_source).unwrap().0.unwrap();
14823        let mut frontend = FrontendState::new();
14824
14825        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14826        let mock_ctx = ExecutorContext::new_mock(None).await;
14827        let version = Version(0);
14828        let project_id = ProjectId(0);
14829        let file_id = FileId(0);
14830
14831        frontend.hack_set_program(&ctx, program).await.unwrap();
14832        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14833        let sketch_id = sketch_object.id;
14834        let sketch = expect_sketch(sketch_object);
14835
14836        // Extract segment IDs before edit_sketch borrows frontend mutably.
14837        let line_id = sketch
14838            .segments
14839            .iter()
14840            .copied()
14841            .find(|seg_id| {
14842                matches!(
14843                    &frontend.scene_graph.objects[seg_id.0].kind,
14844                    ObjectKind::Segment {
14845                        segment: Segment::Line(_)
14846                    }
14847                )
14848            })
14849            .expect("Expected a line segment in sketch");
14850
14851        // Enter sketch edit mode.
14852        frontend
14853            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14854            .await
14855            .unwrap();
14856
14857        // Edit the line.
14858        let line_ctor = LineCtor {
14859            start: Point2d {
14860                x: Expr::Var(Number {
14861                    value: 1.0,
14862                    units: NumericSuffix::Mm,
14863                }),
14864                y: Expr::Var(Number {
14865                    value: 2.0,
14866                    units: NumericSuffix::Mm,
14867                }),
14868            },
14869            end: Point2d {
14870                x: Expr::Var(Number {
14871                    value: 13.0,
14872                    units: NumericSuffix::Mm,
14873                }),
14874                y: Expr::Var(Number {
14875                    value: 14.0,
14876                    units: NumericSuffix::Mm,
14877                }),
14878            },
14879            construction: None,
14880        };
14881        let segments = vec![ExistingSegmentCtor {
14882            id: line_id,
14883            ctor: SegmentCtor::Line(line_ctor),
14884        }];
14885        let (src_delta, _scene_delta) = frontend
14886            .edit_segments(&mock_ctx, version, sketch_id, segments)
14887            .await
14888            .unwrap();
14889        assert!(
14890            src_delta
14891                .text
14892                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14893            "Expected edited line in source, got: {}",
14894            src_delta.text
14895        );
14896
14897        ctx.close().await;
14898        mock_ctx.close().await;
14899    }
14900
14901    #[tokio::test(flavor = "multi_thread")]
14902    async fn test_extra_newlines_delete_segment() {
14903        // Extra whitespace before and after the sketch block.
14904        let initial_source = "@settings(defaultLengthUnit = mm)
14905
14906
14907
14908sketch001 = sketch(on = XY) {
14909  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14910}
14911";
14912
14913        let program = Program::parse(initial_source).unwrap().0.unwrap();
14914        let mut frontend = FrontendState::new();
14915
14916        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14917        let mock_ctx = ExecutorContext::new_mock(None).await;
14918        let version = Version(0);
14919
14920        frontend.hack_set_program(&ctx, program).await.unwrap();
14921        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14922        let sketch_id = sketch_object.id;
14923        let sketch = expect_sketch(sketch_object);
14924
14925        // The sketch should have 3 segments: start point, center point, and the circle.
14926        assert_eq!(sketch.segments.len(), 3);
14927        let circle_id = sketch.segments[2];
14928
14929        // Delete the circle despite extra newlines in original source.
14930        let (src_delta, scene_delta) = frontend
14931            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14932            .await
14933            .unwrap();
14934        assert!(
14935            src_delta.text.contains("sketch(on = XY) {"),
14936            "Expected sketch block in source, got: {}",
14937            src_delta.text
14938        );
14939        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14940        let new_sketch = expect_sketch(new_sketch_object);
14941        assert_eq!(new_sketch.segments.len(), 0);
14942
14943        ctx.close().await;
14944        mock_ctx.close().await;
14945    }
14946
14947    #[tokio::test(flavor = "multi_thread")]
14948    async fn test_unformatted_source_add_arc() {
14949        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14950        let initial_source = "@settings(defaultLengthUnit = mm)
14951
14952
14953
14954
14955sketch001 = sketch(on = XY) {
14956}
14957";
14958
14959        let program = Program::parse(initial_source).unwrap().0.unwrap();
14960        let mut frontend = FrontendState::new();
14961
14962        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14963        let mock_ctx = ExecutorContext::new_mock(None).await;
14964        let version = Version(0);
14965
14966        frontend.hack_set_program(&ctx, program).await.unwrap();
14967        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14968        let sketch_id = sketch_object.id;
14969
14970        let arc_ctor = ArcCtor {
14971            start: Point2d {
14972                x: Expr::Var(Number {
14973                    value: 5.0,
14974                    units: NumericSuffix::Mm,
14975                }),
14976                y: Expr::Var(Number {
14977                    value: 0.0,
14978                    units: NumericSuffix::Mm,
14979                }),
14980            },
14981            end: Point2d {
14982                x: Expr::Var(Number {
14983                    value: 0.0,
14984                    units: NumericSuffix::Mm,
14985                }),
14986                y: Expr::Var(Number {
14987                    value: 5.0,
14988                    units: NumericSuffix::Mm,
14989                }),
14990            },
14991            center: Point2d {
14992                x: Expr::Var(Number {
14993                    value: 0.0,
14994                    units: NumericSuffix::Mm,
14995                }),
14996                y: Expr::Var(Number {
14997                    value: 0.0,
14998                    units: NumericSuffix::Mm,
14999                }),
15000            },
15001            construction: None,
15002        };
15003        let segment = SegmentCtor::Arc(arc_ctor);
15004        let (src_delta, scene_delta) = frontend
15005            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15006            .await
15007            .unwrap();
15008        assert!(
15009            src_delta
15010                .text
15011                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15012            "Expected arc in source, got: {}",
15013            src_delta.text
15014        );
15015        assert!(!scene_delta.new_objects.is_empty());
15016
15017        ctx.close().await;
15018        mock_ctx.close().await;
15019    }
15020
15021    #[tokio::test(flavor = "multi_thread")]
15022    async fn test_extra_newlines_add_circle() {
15023        // Extra blank lines between settings and sketch.
15024        let initial_source = "@settings(defaultLengthUnit = mm)
15025
15026
15027
15028sketch001 = sketch(on = XY) {
15029}
15030";
15031
15032        let program = Program::parse(initial_source).unwrap().0.unwrap();
15033        let mut frontend = FrontendState::new();
15034
15035        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15036        let mock_ctx = ExecutorContext::new_mock(None).await;
15037        let version = Version(0);
15038
15039        frontend.hack_set_program(&ctx, program).await.unwrap();
15040        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15041        let sketch_id = sketch_object.id;
15042
15043        let circle_ctor = CircleCtor {
15044            start: Point2d {
15045                x: Expr::Var(Number {
15046                    value: 5.0,
15047                    units: NumericSuffix::Mm,
15048                }),
15049                y: Expr::Var(Number {
15050                    value: 0.0,
15051                    units: NumericSuffix::Mm,
15052                }),
15053            },
15054            center: Point2d {
15055                x: Expr::Var(Number {
15056                    value: 0.0,
15057                    units: NumericSuffix::Mm,
15058                }),
15059                y: Expr::Var(Number {
15060                    value: 0.0,
15061                    units: NumericSuffix::Mm,
15062                }),
15063            },
15064            construction: None,
15065        };
15066        let segment = SegmentCtor::Circle(circle_ctor);
15067        let (src_delta, scene_delta) = frontend
15068            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15069            .await
15070            .unwrap();
15071        assert!(
15072            src_delta
15073                .text
15074                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15075            "Expected circle in source, got: {}",
15076            src_delta.text
15077        );
15078        assert!(!scene_delta.new_objects.is_empty());
15079
15080        ctx.close().await;
15081        mock_ctx.close().await;
15082    }
15083
15084    #[tokio::test(flavor = "multi_thread")]
15085    async fn test_extra_newlines_add_constraint() {
15086        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15087        let initial_source = "@settings(defaultLengthUnit = mm)
15088
15089
15090
15091sketch001 = sketch(on = XY) {
15092  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15093  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15094}
15095";
15096
15097        let program = Program::parse(initial_source).unwrap().0.unwrap();
15098        let mut frontend = FrontendState::new();
15099
15100        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15101        let mock_ctx = ExecutorContext::new_mock(None).await;
15102        let version = Version(0);
15103        let project_id = ProjectId(0);
15104        let file_id = FileId(0);
15105
15106        frontend.hack_set_program(&ctx, program).await.unwrap();
15107        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15108        let sketch_id = sketch_object.id;
15109        let sketch = expect_sketch(sketch_object);
15110
15111        // Extract segment data before edit_sketch borrows frontend mutably.
15112        let line_ids: Vec<ObjectId> = sketch
15113            .segments
15114            .iter()
15115            .copied()
15116            .filter(|seg_id| {
15117                matches!(
15118                    &frontend.scene_graph.objects[seg_id.0].kind,
15119                    ObjectKind::Segment {
15120                        segment: Segment::Line(_)
15121                    }
15122                )
15123            })
15124            .collect();
15125        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15126
15127        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15128        let ObjectKind::Segment {
15129            segment: Segment::Line(line1_data),
15130        } = &line1.kind
15131        else {
15132            panic!("Expected line");
15133        };
15134        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15135        let ObjectKind::Segment {
15136            segment: Segment::Line(line2_data),
15137        } = &line2.kind
15138        else {
15139            panic!("Expected line");
15140        };
15141
15142        // Build constraint before entering sketch mode.
15143        let constraint = Constraint::Coincident(Coincident {
15144            segments: vec![line1_data.end.into(), line2_data.start.into()],
15145        });
15146
15147        // Enter sketch edit mode.
15148        frontend
15149            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15150            .await
15151            .unwrap();
15152        let (src_delta, _scene_delta) = frontend
15153            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15154            .await
15155            .unwrap();
15156        assert!(
15157            src_delta.text.contains("coincident("),
15158            "Expected coincident constraint in source, got: {}",
15159            src_delta.text
15160        );
15161
15162        ctx.close().await;
15163        mock_ctx.close().await;
15164    }
15165
15166    #[tokio::test(flavor = "multi_thread")]
15167    async fn test_extra_newlines_add_line_then_edit_line() {
15168        // Extra newlines after @settings - add a line, then edit it.
15169        let initial_source = "@settings(defaultLengthUnit = mm)
15170
15171
15172
15173sketch001 = sketch(on = XY) {
15174}
15175";
15176
15177        let program = Program::parse(initial_source).unwrap().0.unwrap();
15178        let mut frontend = FrontendState::new();
15179
15180        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15181        let mock_ctx = ExecutorContext::new_mock(None).await;
15182        let version = Version(0);
15183
15184        frontend.hack_set_program(&ctx, program).await.unwrap();
15185        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15186        let sketch_id = sketch_object.id;
15187
15188        // Add a line.
15189        let line_ctor = LineCtor {
15190            start: Point2d {
15191                x: Expr::Number(Number {
15192                    value: 0.0,
15193                    units: NumericSuffix::Mm,
15194                }),
15195                y: Expr::Number(Number {
15196                    value: 0.0,
15197                    units: NumericSuffix::Mm,
15198                }),
15199            },
15200            end: Point2d {
15201                x: Expr::Number(Number {
15202                    value: 10.0,
15203                    units: NumericSuffix::Mm,
15204                }),
15205                y: Expr::Number(Number {
15206                    value: 10.0,
15207                    units: NumericSuffix::Mm,
15208                }),
15209            },
15210            construction: None,
15211        };
15212        let segment = SegmentCtor::Line(line_ctor);
15213        let (src_delta, scene_delta) = frontend
15214            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15215            .await
15216            .unwrap();
15217        assert!(
15218            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15219            "Expected line in source after add, got: {}",
15220            src_delta.text
15221        );
15222        // Line creates start point, end point, and line segment.
15223        let line_id = *scene_delta.new_objects.last().unwrap();
15224
15225        // Edit the line.
15226        let line_ctor = LineCtor {
15227            start: Point2d {
15228                x: Expr::Number(Number {
15229                    value: 1.0,
15230                    units: NumericSuffix::Mm,
15231                }),
15232                y: Expr::Number(Number {
15233                    value: 2.0,
15234                    units: NumericSuffix::Mm,
15235                }),
15236            },
15237            end: Point2d {
15238                x: Expr::Number(Number {
15239                    value: 13.0,
15240                    units: NumericSuffix::Mm,
15241                }),
15242                y: Expr::Number(Number {
15243                    value: 14.0,
15244                    units: NumericSuffix::Mm,
15245                }),
15246            },
15247            construction: None,
15248        };
15249        let segments = vec![ExistingSegmentCtor {
15250            id: line_id,
15251            ctor: SegmentCtor::Line(line_ctor),
15252        }];
15253        let (src_delta, scene_delta) = frontend
15254            .edit_segments(&mock_ctx, version, sketch_id, segments)
15255            .await
15256            .unwrap();
15257        assert!(
15258            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15259            "Expected edited line in source, got: {}",
15260            src_delta.text
15261        );
15262        assert_eq!(scene_delta.new_objects, vec![]);
15263
15264        ctx.close().await;
15265        mock_ctx.close().await;
15266    }
15267}