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::Angle;
9use kcl_api::ArcCtor;
10use kcl_api::ArcDirection;
11use kcl_api::CapSource;
12use kcl_api::CircleCtor;
13use kcl_api::Coincident;
14use kcl_api::Constraint;
15use kcl_api::ConstraintSegment;
16use kcl_api::ControlPointSplineCtor;
17use kcl_api::Diameter;
18use kcl_api::Distance;
19use kcl_api::EqualRadius;
20use kcl_api::Expr;
21use kcl_api::FixedPoint;
22use kcl_api::Freedom;
23use kcl_api::Horizontal;
24use kcl_api::LineCtor;
25use kcl_api::LinesEqualLength;
26use kcl_api::Midpoint;
27use kcl_api::Number;
28use kcl_api::NumericSuffix;
29use kcl_api::Object;
30use kcl_api::ObjectId;
31use kcl_api::ObjectKind;
32use kcl_api::Parallel;
33use kcl_api::Perpendicular;
34use kcl_api::Plane;
35use kcl_api::Point2d;
36use kcl_api::PointCtor;
37use kcl_api::Radius;
38use kcl_api::Segment;
39use kcl_api::SegmentCtor;
40use kcl_api::SketchCtor;
41use kcl_api::SourceRef;
42use kcl_api::SourceRefRange;
43use kcl_api::Symmetric;
44use kcl_api::Tangent;
45use kcl_api::UnitLength;
46use kcl_api::Vertical;
47use kcl_api::WallSource;
48use kcl_error::CompilationIssue;
49use kcl_error::SourceRange;
50use serde::Serialize;
51use uuid::Uuid;
52
53use crate::ExecOutcome;
54use crate::ExecutorContext;
55use crate::KclError;
56use crate::KclErrorWithOutputs;
57use crate::KclValueView;
58use crate::Program;
59use crate::SegmentDragAnchor;
60use crate::collections::AhashIndexSet;
61use crate::execution::Artifact;
62use crate::execution::ArtifactGraph;
63use crate::execution::ArtifactId;
64use crate::execution::CapSubType;
65use crate::execution::CodeRef;
66use crate::execution::MockConfig;
67use crate::execution::SKETCH_BLOCK_PARAM_ON;
68use crate::execution::annotations::WarningLevel;
69use crate::execution::cache::SketchModeState;
70use crate::execution::types::adjust_length;
71use crate::fmt::format_number_literal;
72use crate::front::CoincidentExt;
73use crate::front::DistanceExt;
74use crate::front::Error;
75use crate::front::ExecResult;
76use crate::front::ObjectKindExt;
77use crate::front::SegmentExt;
78use crate::frontend::api::FileId;
79use crate::frontend::api::ProjectId;
80use crate::frontend::api::RestoreSketchCheckpointOutcome;
81use crate::frontend::api::SceneGraph;
82use crate::frontend::api::SceneGraphDelta;
83use crate::frontend::api::SketchCheckpointId;
84use crate::frontend::api::SourceDelta;
85use crate::frontend::api::Version;
86use crate::frontend::modify::find_defined_names;
87use crate::frontend::modify::next_free_name;
88use crate::frontend::modify::next_free_name_with_padding;
89use crate::frontend::sketch::ConstraintLabelPositionEdit;
90use crate::frontend::sketch::ExistingSegmentCtor;
91use crate::frontend::sketch::SketchApi;
92use crate::id::IncIdGenerator;
93use crate::parsing::ast::types as ast;
94use crate::parsing::ast::types::BoxNode;
95use crate::parsing::ast::types::CallExpressionKw;
96use crate::parsing::ast::types::NodePathExt;
97use crate::std::constraints::LinesAtAngleKind;
98use crate::walk::NodeMut;
99use crate::walk::Visitable;
100use crate::walk::traverse::MutateBodyItem;
101use crate::walk::traverse::TraversalReturn;
102use crate::walk::traverse::Visitor;
103use crate::walk::traverse::dfs_mut;
104
105pub(crate) mod api;
106pub(crate) mod modify;
107pub(crate) mod sketch;
108
109pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
110
111#[derive(Debug, Clone)]
112struct SketchCheckpoint {
113    id: SketchCheckpointId,
114    source: SourceDelta,
115    program: Program,
116    scene_graph: SceneGraph,
117    exec_outcome: ExecOutcome,
118    point_freedom_cache: HashMap<ObjectId, Freedom>,
119    mock_memory: Option<SketchModeState>,
120}
121pub(crate) mod trim;
122
123struct ArcSizeConstraintParams {
124    points: Vec<ObjectId>,
125    function_name: &'static str,
126    value: f64,
127    units: NumericSuffix,
128    label_position: Option<Point2d<Number>>,
129    constraint_type_name: &'static str,
130}
131
132const POINT_FN: &str = "point";
133const POINT_AT_PARAM: &str = "at";
134const LINE_FN: &str = "line";
135const LINE_VARIABLE: &str = "line";
136const LINE_START_PARAM: &str = "start";
137const LINE_END_PARAM: &str = "end";
138const ARC_FN: &str = "arc";
139const ARC_VARIABLE: &str = "arc";
140const ARC_START_PARAM: &str = "start";
141const ARC_END_PARAM: &str = "end";
142const ARC_CENTER_PARAM: &str = "center";
143const ARC_DIRECTION_PARAM: &str = "direction";
144/// The name of the KCL std constant for a clockwise arc direction.
145const ARC_DIRECTION_CW_NAME: &str = "CW";
146const CIRCLE_FN: &str = "circle";
147const CIRCLE_VARIABLE: &str = "circle";
148const CIRCLE_START_PARAM: &str = "start";
149const CIRCLE_CENTER_PARAM: &str = "center";
150const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
151const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
152const LABEL_POSITION_PARAM: &str = "labelPosition";
153
154const COINCIDENT_FN: &str = "coincident";
155const DIAMETER_FN: &str = "diameter";
156const DISTANCE_FN: &str = "distance";
157const FIXED_FN: &str = "fixed";
158const ANGLE_FN: &str = "angle";
159const ANGLE_DIMENSION_FN: &str = "angleDimension";
160const ANGLE_LINES_PARAM: &str = "lines";
161const ANGLE_SECTOR_PARAM: &str = "sector";
162const ANGLE_INVERSE_PARAM: &str = "inverse";
163const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
164const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
165const EQUAL_LENGTH_FN: &str = "equalLength";
166const EQUAL_RADIUS_FN: &str = "equalRadius";
167const HORIZONTAL_FN: &str = "horizontal";
168const MIDPOINT_FN: &str = "midpoint";
169const MIDPOINT_POINT_PARAM: &str = "point";
170const RADIUS_FN: &str = "radius";
171const SYMMETRIC_FN: &str = "symmetric";
172const SYMMETRIC_AXIS_PARAM: &str = "axis";
173const TANGENT_FN: &str = "tangent";
174const VERTICAL_FN: &str = "vertical";
175
176const LINE_PROPERTY_START: &str = "start";
177const LINE_PROPERTY_END: &str = "end";
178
179const ARC_PROPERTY_START: &str = "start";
180const ARC_PROPERTY_END: &str = "end";
181const ARC_PROPERTY_CENTER: &str = "center";
182const CIRCLE_PROPERTY_START: &str = "start";
183const CIRCLE_PROPERTY_CENTER: &str = "center";
184const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
185const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
186
187const CONSTRUCTION_PARAM: &str = "construction";
188
189#[derive(Debug, Clone, Copy)]
190enum EditDeleteKind {
191    Edit,
192    DeleteNonSketch,
193}
194
195/// Options that control how an edit is re-executed and written back.
196struct ExecuteAfterEditOptions {
197    segment_ids_edited: AhashIndexSet<ObjectId>,
198    edit_kind: EditDeleteKind,
199    commit_solved_initial_guesses: bool,
200}
201
202impl EditDeleteKind {
203    /// Returns true if this edit is any type of deletion.
204    fn is_delete(&self) -> bool {
205        match self {
206            EditDeleteKind::Edit => false,
207            EditDeleteKind::DeleteNonSketch => true,
208        }
209    }
210
211    fn to_change_kind(self) -> ChangeKind {
212        match self {
213            EditDeleteKind::Edit => ChangeKind::Edit,
214            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
215        }
216    }
217}
218
219#[derive(Debug, Clone, Copy)]
220enum ChangeKind {
221    Add,
222    Edit,
223    Delete,
224    None,
225}
226
227#[derive(Debug, Clone, Serialize, ts_rs::TS)]
228#[ts(export, export_to = "FrontendApi.ts")]
229#[serde(tag = "type")]
230pub enum SetProgramOutcome {
231    #[serde(rename_all = "camelCase")]
232    Success {
233        scene_graph: Box<SceneGraph>,
234        exec_outcome: Box<ExecOutcome>,
235        checkpoint_id: Option<SketchCheckpointId>,
236    },
237    #[serde(rename_all = "camelCase")]
238    ExecFailure { error: Box<KclErrorWithOutputs> },
239}
240
241/// Options for a sketch segment edit that participates in drag solving.
242pub struct EditSegmentsOptions {
243    /// Narrows which edited scene objects receive temporary fixed constraints.
244    ///
245    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
246    /// disables those fixed constraints, which is useful for semantic edits such
247    /// as toggling construction state.
248    pub anchor_segment_ids: Option<Vec<ObjectId>>,
249    /// Hidden fixed cursor points that the referenced segment bodies must pass
250    /// through during solve.
251    pub drag_anchors: Vec<SegmentDragAnchor>,
252    /// Constraint label positions to write in the same AST transaction as the
253    /// segment edits. Label positions do not participate in solving.
254    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
255    /// Whether solver-updated initial guesses should be written back to KCL.
256    pub commit_solved_initial_guesses: bool,
257}
258
259/// Options for a distance-constraint label edit during sketch dragging.
260pub struct EditDistanceConstraintLabelPositionOptions {
261    /// Edited scene objects to keep anchored while previewing the label edit.
262    pub anchor_segment_ids: Vec<ObjectId>,
263    /// Whether solver-updated initial guesses should be written back to KCL.
264    pub commit_solved_initial_guesses: bool,
265}
266
267/// Options for editing a constraint during sketch dragging.
268pub struct EditConstraintOptions {
269    /// Whether solver-updated initial guesses should be written back to KCL.
270    pub commit_solved_initial_guesses: bool,
271}
272
273#[derive(Debug, Clone)]
274struct SolidAstReference {
275    variable_name: String,
276    output_index: Option<usize>,
277}
278
279#[derive(Debug, Clone)]
280pub struct FrontendState {
281    program: Program,
282    scene_graph: SceneGraph,
283    /// Lightweight map from engine solid IDs to their latest KCL references.
284    solid_references: HashMap<Uuid, SolidAstReference>,
285    /// Stores the last known freedom value for each point object.
286    /// This allows us to preserve freedom values when freedom analysis isn't run.
287    point_freedom_cache: HashMap<ObjectId, Freedom>,
288    /// One-shot drag anchors for the next segment edit. These ids define which
289    /// edited points/segments become temporary fixed constraints during solve.
290    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
291    /// One-shot segment-body drag anchors for the next segment edit. These add
292    /// a temporary solver point on the dragged segment that follows the cursor.
293    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
294    /// One-shot constraint label edits to apply with the next segment edit.
295    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
296    /// One-shot override for whether the next edit commits solver-updated
297    /// initial guesses back into KCL. Drag previews keep this off so only the
298    /// explicit drag edit feeds the next solve.
299    next_edit_commits_solver_solutions: Option<bool>,
300    sketch_checkpoints: VecDeque<SketchCheckpoint>,
301    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
302}
303
304impl Default for FrontendState {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310impl FrontendState {
311    pub fn new() -> Self {
312        Self {
313            program: Program::empty(),
314            scene_graph: SceneGraph {
315                project: ProjectId(0),
316                file: FileId(0),
317                version: Version(0),
318                objects: Default::default(),
319                settings: Default::default(),
320                sketch_mode: Default::default(),
321            },
322            solid_references: HashMap::new(),
323            point_freedom_cache: HashMap::new(),
324            next_drag_anchor_segment_ids: None,
325            next_segment_drag_anchors: None,
326            next_constraint_label_edits: None,
327            next_edit_commits_solver_solutions: None,
328            sketch_checkpoints: VecDeque::new(),
329            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
330        }
331    }
332
333    /// Get a reference to the scene graph
334    pub fn scene_graph(&self) -> &SceneGraph {
335        &self.scene_graph
336    }
337
338    pub fn default_length_unit(&self) -> UnitLength {
339        self.program
340            .meta_settings()
341            .ok()
342            .flatten()
343            .map(|settings| settings.default_length_units)
344            .unwrap_or(UnitLength::Millimeters)
345    }
346
347    pub async fn create_sketch_checkpoint(
348        &mut self,
349        cache: &crate::Cache,
350        exec_outcome: ExecOutcome,
351    ) -> api::Result<SketchCheckpointId> {
352        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
353
354        let checkpoint = SketchCheckpoint {
355            id: checkpoint_id,
356            source: SourceDelta {
357                text: source_from_ast(&self.program.ast),
358            },
359            program: self.program.clone(),
360            scene_graph: self.scene_graph.clone(),
361            exec_outcome,
362            point_freedom_cache: self.point_freedom_cache.clone(),
363            mock_memory: cache.read_old_memory().await,
364        };
365
366        self.sketch_checkpoints.push_back(checkpoint);
367        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
368            self.sketch_checkpoints.pop_front();
369        }
370
371        Ok(checkpoint_id)
372    }
373
374    /// Edit sketch segments with optional drag-solve overrides.
375    ///
376    /// Drag anchors add hidden fixed cursor points and constrain the referenced
377    /// segment bodies to pass through them, which lets body drags use the same
378    /// anchor model without pinning all child points. Preview callers disable
379    /// solver writeback so solved geometry can be returned without feeding every
380    /// solver value back into KCL.
381    pub async fn edit_segments_with_options(
382        &mut self,
383        ctx: &ExecutorContext,
384        version: Version,
385        sketch: ObjectId,
386        segments: Vec<ExistingSegmentCtor>,
387        options: EditSegmentsOptions,
388    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
389        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
390            self.next_drag_anchor_segment_ids
391                .replace(anchor_ids.into_iter().collect())
392        });
393        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
394        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
395        let previous_commit_mode = self
396            .next_edit_commits_solver_solutions
397            .replace(options.commit_solved_initial_guesses);
398        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
399        if let Some(previous_anchor_ids) = previous_anchor_ids {
400            self.next_drag_anchor_segment_ids = previous_anchor_ids;
401        }
402        self.next_segment_drag_anchors = previous_drag_anchors;
403        self.next_constraint_label_edits = previous_constraint_label_edits;
404        self.next_edit_commits_solver_solutions = previous_commit_mode;
405        result
406    }
407
408    /// Edit a distance-constraint label position with optional solver writeback.
409    ///
410    /// Drag previews set `commit_solved_initial_guesses` to false so label
411    /// placement can be previewed against solved geometry without advancing
412    /// persistent KCL state until drag completion.
413    pub async fn edit_distance_constraint_label_position_with_options(
414        &mut self,
415        ctx: &ExecutorContext,
416        version: Version,
417        sketch: ObjectId,
418        constraint_id: ObjectId,
419        label_position: Point2d<Number>,
420        options: EditDistanceConstraintLabelPositionOptions,
421    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
422        let previous_commit_mode = self
423            .next_edit_commits_solver_solutions
424            .replace(options.commit_solved_initial_guesses);
425        let result = SketchApi::edit_distance_constraint_label_position(
426            self,
427            ctx,
428            version,
429            sketch,
430            constraint_id,
431            label_position,
432            options.anchor_segment_ids,
433        )
434        .await;
435        self.next_edit_commits_solver_solutions = previous_commit_mode;
436        result
437    }
438
439    /// Edit a distance constraint with optional solver writeback.
440    pub async fn edit_distance_constraint_with_options(
441        &mut self,
442        ctx: &ExecutorContext,
443        version: Version,
444        sketch: ObjectId,
445        constraint_id: ObjectId,
446        constraint: Constraint,
447        options: EditConstraintOptions,
448    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
449        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
450            .await
451    }
452
453    /// Edit an angle constraint with optional solver writeback.
454    pub async fn edit_angle_constraint_with_options(
455        &mut self,
456        ctx: &ExecutorContext,
457        version: Version,
458        sketch: ObjectId,
459        constraint_id: ObjectId,
460        angle: Angle,
461        options: EditConstraintOptions,
462    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
463        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
464            .await
465    }
466
467    /// Edit an angle or distance-family constraint with optional solver writeback.
468    pub async fn edit_constraint_with_options(
469        &mut self,
470        ctx: &ExecutorContext,
471        _version: Version,
472        sketch: ObjectId,
473        constraint_id: ObjectId,
474        constraint: Constraint,
475        options: EditConstraintOptions,
476    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
477        // TODO: Check version.
478        let sketch_block_ref =
479            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
480
481        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
482            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
483        })?;
484
485        let mut new_ast = self.program.ast.clone();
486        let command = match &object.kind {
487            ObjectKind::Constraint {
488                constraint:
489                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
490            } => {
491                let (function_name, distance) = match &constraint {
492                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
493                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
494                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
495                    _ => {
496                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
497                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
498                        )));
499                    }
500                };
501                let (call, value) = self
502                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
503                    .map_err(KclErrorWithOutputs::no_outputs)?;
504                AstMutateCommand::EditDistanceConstraint { call, value }
505            }
506            ObjectKind::Constraint {
507                constraint: Constraint::Angle(_),
508            } => {
509                let Constraint::Angle(angle) = &constraint else {
510                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
511                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
512                    )));
513                };
514                let (call, value) = self
515                    .angle_constraint_ast_parts(angle, &mut new_ast)
516                    .map_err(KclErrorWithOutputs::no_outputs)?;
517                AstMutateCommand::EditAngleConstraint { call, value }
518            }
519            ObjectKind::Constraint { .. } => {
520                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
521                    "Editing {} is not supported",
522                    object.kind.human_friendly_kind_with_article(),
523                ))));
524            }
525            _ => {
526                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
527                    "Object is not a constraint: {constraint_id:?}"
528                ))));
529            }
530        };
531
532        self.mutate_ast(&mut new_ast, constraint_id, command)
533            .map_err(KclErrorWithOutputs::no_outputs)?;
534
535        self.execute_after_edit(
536            ctx,
537            sketch,
538            sketch_block_ref,
539            &mut new_ast,
540            ExecuteAfterEditOptions {
541                segment_ids_edited: Default::default(),
542                edit_kind: EditDeleteKind::Edit,
543                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
544            },
545        )
546        .await
547    }
548
549    pub async fn restore_sketch_checkpoint(
550        &mut self,
551        cache: &crate::Cache,
552        checkpoint_id: SketchCheckpointId,
553    ) -> api::Result<RestoreSketchCheckpointOutcome> {
554        let checkpoint = self
555            .sketch_checkpoints
556            .iter()
557            .find(|checkpoint| checkpoint.id == checkpoint_id)
558            .cloned()
559            .ok_or_else(|| Error {
560                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
561            })?;
562
563        self.program = checkpoint.program;
564        self.scene_graph = checkpoint.scene_graph.clone();
565        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
566        self.point_freedom_cache = checkpoint.point_freedom_cache;
567        self.next_drag_anchor_segment_ids = None;
568        self.next_segment_drag_anchors = None;
569        self.next_constraint_label_edits = None;
570        self.next_edit_commits_solver_solutions = None;
571
572        if let Some(mock_memory) = checkpoint.mock_memory {
573            cache.write_old_memory(mock_memory).await;
574        } else {
575            cache.clear_mem_cache().await;
576        }
577
578        Ok(RestoreSketchCheckpointOutcome {
579            source_delta: checkpoint.source,
580            scene_graph_delta: SceneGraphDelta {
581                new_graph: self.scene_graph_for_ui(),
582                new_objects: Vec::new(),
583                invalidates_ids: true,
584                exec_outcome: checkpoint.exec_outcome,
585            },
586        })
587    }
588
589    pub fn clear_sketch_checkpoints(&mut self) {
590        self.sketch_checkpoints.clear();
591    }
592    fn scene_graph_for_ui(&self) -> SceneGraph {
593        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
594            matches!(
595                object.kind,
596                ObjectKind::Segment {
597                    segment: Segment::ControlPointSpline(_)
598                }
599            )
600        });
601
602        if !has_control_point_splines {
603            return self.scene_graph.clone();
604        }
605
606        let hidden_constraint_ids = self
607            .scene_graph
608            .objects
609            .iter()
610            .filter_map(|object| match &object.kind {
611                ObjectKind::Constraint {
612                    constraint: Constraint::Coincident(coincident),
613                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
614                    Some(object.id)
615                }
616                _ => None,
617            })
618            .collect::<HashSet<_>>();
619
620        if hidden_constraint_ids.is_empty() {
621            return self.scene_graph.clone();
622        }
623
624        let mut scene_graph = self.scene_graph.clone();
625        for object in &mut scene_graph.objects {
626            match &mut object.kind {
627                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
628                    object.kind = ObjectKind::Nil;
629                }
630                ObjectKind::Sketch(sketch) => {
631                    sketch
632                        .constraints
633                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
634                }
635                _ => {}
636            }
637        }
638
639        scene_graph
640    }
641}
642
643fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
644    let mut first_owner_id = None;
645    for segment_id in coincident.segment_ids() {
646        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
647            return false;
648        };
649
650        match first_owner_id {
651            Some(first_owner_id) if first_owner_id != owner_id => return false,
652            Some(_) => {}
653            None => first_owner_id = Some(owner_id),
654        }
655    }
656
657    first_owner_id.is_some()
658}
659
660fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
661    let object = scene_graph.objects.get(segment_id.0)?;
662    let ObjectKind::Segment { segment } = &object.kind else {
663        return None;
664    };
665
666    match segment {
667        Segment::ControlPointSpline(_) => Some(segment_id),
668        Segment::Point(point) => point
669            .owner
670            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
671        Segment::Line(line) => line
672            .owner
673            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
674        _ => None,
675    }
676}
677
678fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
679    matches!(
680        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
681        Some(ObjectKind::Segment {
682            segment: Segment::ControlPointSpline(_)
683        })
684    )
685}
686
687fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
688    let experimental_features_allowed = program
689        .meta_settings()
690        .ok()
691        .flatten()
692        .map(|settings| settings.experimental_features == WarningLevel::Allow)
693        .unwrap_or(false);
694    if experimental_features_allowed {
695        return Ok(program.clone());
696    }
697
698    program.change_experimental_features(Some(WarningLevel::Allow))
699}
700
701impl SketchApi for FrontendState {
702    async fn execute_mock(
703        &mut self,
704        ctx: &ExecutorContext,
705        _version: Version,
706        sketch: ObjectId,
707    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
708        let sketch_block_ref =
709            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
710
711        let mut truncated_program = self.program.clone();
712        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
713            .map_err(KclErrorWithOutputs::no_outputs)?;
714
715        // Execute.
716        let outcome = ctx
717            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
718            .await?;
719        let new_source = source_from_ast(&self.program.ast);
720        let src_delta = SourceDelta { text: new_source };
721        // MockConfig::default() has freedom_analysis: true
722        let outcome = self.update_state_after_exec(outcome, true);
723        let scene_graph_delta = SceneGraphDelta {
724            new_graph: self.scene_graph.clone(),
725            new_objects: Default::default(),
726            invalidates_ids: false,
727            exec_outcome: outcome,
728        };
729        Ok((src_delta, scene_graph_delta))
730    }
731
732    async fn new_sketch(
733        &mut self,
734        ctx: &ExecutorContext,
735        _project: ProjectId,
736        _file: FileId,
737        _version: Version,
738        args: SketchCtor,
739    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
740        // TODO: Check version.
741
742        let mut new_ast = self.program.ast.clone();
743        // Create updated KCL source from args.
744        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
745            .map_err(KclErrorWithOutputs::no_outputs)?;
746        let mut defined_names = find_defined_names(&new_ast);
747        let is_face_of_expr = matches!(
748            &plane_ast,
749            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
750        );
751        if is_face_of_expr {
752            let face_name = next_free_name_with_padding("face", &defined_names)
753                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
754            let face_decl = ast::VariableDeclaration::new(
755                ast::VariableDeclarator::new(&face_name, plane_ast),
756                ast::ItemVisibility::Default,
757                ast::VariableKind::Const,
758            );
759            new_ast
760                .body
761                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
762                    face_decl,
763                ))));
764            defined_names.insert(face_name.clone());
765            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
766        }
767        let sketch_ast = ast::SketchBlock {
768            arguments: vec![ast::LabeledArg {
769                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
770                arg: plane_ast,
771            }],
772            body: Default::default(),
773            is_being_edited: false,
774            non_code_meta: Default::default(),
775            digest: None,
776        };
777        // Add a sketch block as a variable declaration directly, avoiding
778        // source-range mutation on a no-src node.
779        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
780            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
781        let sketch_decl = ast::VariableDeclaration::new(
782            ast::VariableDeclarator::new(
783                &sketch_name,
784                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
785            ),
786            ast::ItemVisibility::Default,
787            ast::VariableKind::Const,
788        );
789        new_ast
790            .body
791            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
792                sketch_decl,
793            ))));
794        // Convert to string source to create real source ranges.
795        let new_source = source_from_ast(&new_ast);
796        // Parse the new source.
797        let new_program = parse_frontend_mutation_source(
798            &new_source,
799            "Error parsing KCL source after adding sketch",
800            "No AST produced after adding sketch",
801        )?;
802
803        // Make sure to only set this if there are no errors.
804        self.program = new_program.clone();
805
806        // We need to do an engine execute so that the plane object gets created
807        // and is cached.
808        let outcome = ctx.run_with_caching(new_program.clone()).await?;
809        let freedom_analysis_ran = true;
810
811        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
812
813        let Some(sketch_id) = self
814            .scene_graph
815            .objects
816            .iter()
817            .filter_map(|object| match object.kind {
818                ObjectKind::Sketch(_) => Some(object.id),
819                _ => None,
820            })
821            .max_by_key(|id| id.0)
822        else {
823            return Err(KclErrorWithOutputs::from_error_outcome(
824                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
825                outcome,
826            ));
827        };
828        // Store the object in the scene.
829        self.scene_graph.sketch_mode = Some(sketch_id);
830
831        let src_delta = SourceDelta { text: new_source };
832        let scene_graph_delta = SceneGraphDelta {
833            new_graph: self.scene_graph_for_ui(),
834            invalidates_ids: false,
835            new_objects: vec![sketch_id],
836            exec_outcome: outcome,
837        };
838        Ok((src_delta, scene_graph_delta, sketch_id))
839    }
840
841    async fn edit_sketch(
842        &mut self,
843        ctx: &ExecutorContext,
844        _project: ProjectId,
845        _file: FileId,
846        _version: Version,
847        sketch: ObjectId,
848    ) -> ExecResult<SceneGraphDelta> {
849        // TODO: Check version.
850
851        // Look up existing sketch.
852        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
853            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
854        })?;
855        let ObjectKind::Sketch(_) = &sketch_object.kind else {
856            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
857                "Object is not a sketch, it is {}",
858                sketch_object.kind.human_friendly_kind_with_article()
859            ))));
860        };
861        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
862
863        // Enter sketch mode by setting the sketch_mode.
864        self.scene_graph.sketch_mode = Some(sketch);
865
866        // Truncate after the sketch block for mock execution.
867        let mut truncated_program = self.program.clone();
868        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
869            .map_err(KclErrorWithOutputs::no_outputs)?;
870
871        // Execute in mock mode to ensure state is up to date. The caller will
872        // want freedom analysis to display segments correctly.
873        let outcome = ctx
874            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
875            .await?;
876
877        // MockConfig::default() has freedom_analysis: true
878        let outcome = self.update_state_after_exec(outcome, true);
879        let scene_graph_delta = SceneGraphDelta {
880            new_graph: self.scene_graph_for_ui(),
881            invalidates_ids: false,
882            new_objects: Vec::new(),
883            exec_outcome: outcome,
884        };
885        Ok(scene_graph_delta)
886    }
887
888    async fn exit_sketch(
889        &mut self,
890        ctx: &ExecutorContext,
891        _version: Version,
892        sketch: ObjectId,
893    ) -> ExecResult<SceneGraph> {
894        // TODO: Check version.
895        #[cfg(not(target_arch = "wasm32"))]
896        let _ = sketch;
897        #[cfg(target_arch = "wasm32")]
898        if self.scene_graph.sketch_mode != Some(sketch) {
899            web_sys::console::warn_1(
900                &format!(
901                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
902                    self.scene_graph.sketch_mode
903                )
904                .into(),
905            );
906        }
907        self.scene_graph.sketch_mode = None;
908
909        // Execute.
910        let outcome = ctx.run_with_caching(self.program.clone()).await?;
911
912        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
913        self.update_state_after_exec(outcome, false);
914
915        Ok(self.scene_graph_for_ui())
916    }
917
918    async fn delete_sketch(
919        &mut self,
920        ctx: &ExecutorContext,
921        _version: Version,
922        sketch: ObjectId,
923    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
924        // TODO: Check version.
925
926        let mut new_ast = self.program.ast.clone();
927
928        // Look up existing sketch.
929        let sketch_id = sketch;
930        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
931            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
932        })?;
933        let ObjectKind::Sketch(_) = &sketch_object.kind else {
934            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
935                "Object is not a sketch, it is {}",
936                sketch_object.kind.human_friendly_kind_with_article(),
937            ))));
938        };
939
940        // Modify the AST to remove the sketch.
941        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
942            .map_err(KclErrorWithOutputs::no_outputs)?;
943
944        self.execute_after_delete_sketch(ctx, &mut new_ast).await
945    }
946
947    async fn add_segment(
948        &mut self,
949        ctx: &ExecutorContext,
950        _version: Version,
951        sketch: ObjectId,
952        segment: SegmentCtor,
953        _label: Option<String>,
954    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
955        // TODO: Check version.
956        match segment {
957            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
958            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
959            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
960            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
961            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
962        }
963    }
964
965    async fn edit_segments(
966        &mut self,
967        ctx: &ExecutorContext,
968        _version: Version,
969        sketch: ObjectId,
970        segments: Vec<ExistingSegmentCtor>,
971    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
972        // TODO: Check version.
973        let sketch_block_ref =
974            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
975
976        let mut new_ast = self.program.ast.clone();
977        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
978        let mut invalidates_ids = false;
979
980        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
981        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
982        for segment in &segments {
983            edited_segment_ids.insert(segment.id);
984            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
985                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
986                && let ObjectKind::Segment {
987                    segment: Segment::ControlPointSpline(existing_spline),
988                } = &existing_object.kind
989                && existing_spline.controls.len() != new_ctor.points.len()
990            {
991                invalidates_ids = true;
992            }
993        }
994        let drag_anchor_segment_ids = self
995            .next_drag_anchor_segment_ids
996            .take()
997            .unwrap_or_else(|| edited_segment_ids.clone());
998        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
999        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1000
1001        // Preprocess segments into a final_edits vector to handle if segments contains:
1002        // - edit start point of line1 (as SegmentCtor::Point)
1003        // - edit end point of line1 (as SegmentCtor::Point)
1004        //
1005        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1006        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1007        //
1008        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1009        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1010        // so the above example would result in a single line1 edit:
1011        // - the first start point edit creates a new line edit entry in final_edits
1012        // - the second end point edit finds this line edit and mutates the end position only.
1013        //
1014        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1015        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1016
1017        for segment in segments {
1018            let segment_id = segment.id;
1019            match segment.ctor {
1020                SegmentCtor::Point(ctor) => {
1021                    // Find the owner, if any (point -> line / arc)
1022                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1023                        && let ObjectKind::Segment { segment } = &segment_object.kind
1024                        && let Segment::Point(point) = segment
1025                        && let Some(owner_id) = point.owner
1026                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1027                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1028                    {
1029                        match owner_segment {
1030                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1031                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1032                                    let SegmentCtor::Line(line_ctor) = existing else {
1033                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1034                                            "Internal: Expected line ctor for owner, but found {}",
1035                                            existing.human_friendly_kind_with_article()
1036                                        ))));
1037                                    };
1038                                    // Line owner is already in final_edits -> apply this point edit
1039                                    if line.start == segment_id {
1040                                        line_ctor.start = ctor.position;
1041                                    } else {
1042                                        line_ctor.end = ctor.position;
1043                                    }
1044                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1045                                    // Line owner is not in final_edits yet -> create it
1046                                    let mut line_ctor = line_ctor.clone();
1047                                    if line.start == segment_id {
1048                                        line_ctor.start = ctor.position;
1049                                    } else {
1050                                        line_ctor.end = ctor.position;
1051                                    }
1052                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1053                                } else {
1054                                    // This should never run..
1055                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1056                                        "Internal: Line does not have line ctor, but found {}",
1057                                        line.ctor.human_friendly_kind_with_article()
1058                                    ))));
1059                                }
1060                                continue;
1061                            }
1062                            Segment::Arc(arc)
1063                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1064                            {
1065                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1066                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1067                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1068                                            "Internal: Expected arc ctor for owner, but found {}",
1069                                            existing.human_friendly_kind_with_article()
1070                                        ))));
1071                                    };
1072                                    if arc.start == segment_id {
1073                                        arc_ctor.start = ctor.position;
1074                                    } else if arc.end == segment_id {
1075                                        arc_ctor.end = ctor.position;
1076                                    } else {
1077                                        arc_ctor.center = ctor.position;
1078                                    }
1079                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1080                                    let mut arc_ctor = arc_ctor.clone();
1081                                    if arc.start == segment_id {
1082                                        arc_ctor.start = ctor.position;
1083                                    } else if arc.end == segment_id {
1084                                        arc_ctor.end = ctor.position;
1085                                    } else {
1086                                        arc_ctor.center = ctor.position;
1087                                    }
1088                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1089                                } else {
1090                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1091                                        "Internal: Arc does not have arc ctor, but found {}",
1092                                        arc.ctor.human_friendly_kind_with_article()
1093                                    ))));
1094                                }
1095                                continue;
1096                            }
1097                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1098                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1099                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1100                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1101                                            "Internal: Expected circle ctor for owner, but found {}",
1102                                            existing.human_friendly_kind_with_article()
1103                                        ))));
1104                                    };
1105                                    if circle.start == segment_id {
1106                                        circle_ctor.start = ctor.position;
1107                                    } else {
1108                                        circle_ctor.center = ctor.position;
1109                                    }
1110                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1111                                    let mut circle_ctor = circle_ctor.clone();
1112                                    if circle.start == segment_id {
1113                                        circle_ctor.start = ctor.position;
1114                                    } else {
1115                                        circle_ctor.center = ctor.position;
1116                                    }
1117                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1118                                } else {
1119                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1120                                        "Internal: Circle does not have circle ctor, but found {}",
1121                                        circle.ctor.human_friendly_kind_with_article()
1122                                    ))));
1123                                }
1124                                continue;
1125                            }
1126                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1127                                let Some(control_index) =
1128                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1129                                else {
1130                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1132                                    ))));
1133                                };
1134                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1135                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1136                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1137                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1138                                            existing.human_friendly_kind_with_article()
1139                                        ))));
1140                                    };
1141                                    spline_ctor.points[control_index] = ctor.position;
1142                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1143                                    let mut spline_ctor = spline_ctor.clone();
1144                                    spline_ctor.points[control_index] = ctor.position;
1145                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1146                                } else {
1147                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1148                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1149                                        spline.ctor.human_friendly_kind_with_article()
1150                                    ))));
1151                                }
1152                                continue;
1153                            }
1154                            _ => {}
1155                        }
1156                    }
1157
1158                    // No owner, it's an individual point
1159                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1160                }
1161                SegmentCtor::Line(ctor) => {
1162                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1163                }
1164                SegmentCtor::Arc(ctor) => {
1165                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1166                }
1167                SegmentCtor::Circle(ctor) => {
1168                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1169                }
1170                SegmentCtor::ControlPointSpline(ctor) => {
1171                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1172                }
1173            }
1174        }
1175
1176        for (segment_id, ctor) in final_edits {
1177            match ctor {
1178                SegmentCtor::Point(ctor) => self
1179                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1180                    .map_err(KclErrorWithOutputs::no_outputs)?,
1181                SegmentCtor::Line(ctor) => self
1182                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1183                    .map_err(KclErrorWithOutputs::no_outputs)?,
1184                SegmentCtor::Arc(ctor) => self
1185                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1186                    .map_err(KclErrorWithOutputs::no_outputs)?,
1187                SegmentCtor::Circle(ctor) => self
1188                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1189                    .map_err(KclErrorWithOutputs::no_outputs)?,
1190                SegmentCtor::ControlPointSpline(ctor) => self
1191                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1192                    .map_err(KclErrorWithOutputs::no_outputs)?,
1193            }
1194        }
1195        for edit in constraint_label_edits {
1196            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1197                .map_err(KclErrorWithOutputs::no_outputs)?;
1198        }
1199        let (source_delta, mut scene_graph_delta) = self
1200            .execute_after_edit(
1201                ctx,
1202                sketch,
1203                sketch_block_ref,
1204                &mut new_ast,
1205                ExecuteAfterEditOptions {
1206                    segment_ids_edited: drag_anchor_segment_ids,
1207                    edit_kind: EditDeleteKind::Edit,
1208                    commit_solved_initial_guesses,
1209                },
1210            )
1211            .await?;
1212        if invalidates_ids {
1213            scene_graph_delta.invalidates_ids = true;
1214        }
1215        Ok((source_delta, scene_graph_delta))
1216    }
1217
1218    async fn delete_objects(
1219        &mut self,
1220        ctx: &ExecutorContext,
1221        _version: Version,
1222        sketch: ObjectId,
1223        constraint_ids: Vec<ObjectId>,
1224        segment_ids: Vec<ObjectId>,
1225    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1226        // TODO: Check version.
1227        let sketch_block_ref =
1228            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1229
1230        // Deduplicate IDs.
1231        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1232        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1233
1234        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1235        // also delete the point, as well as other points that are owned by the owner.
1236        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1237
1238        for segment_id in segment_ids_set.iter().copied() {
1239            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1240                let ObjectKind::Segment { segment } = &segment_object.kind else {
1241                    return None;
1242                };
1243                match segment {
1244                    Segment::Point(point) => point.owner,
1245                    Segment::Line(line) => line.owner,
1246                    _ => None,
1247                }
1248            });
1249
1250            if let Some(owner_id) = owner_id
1251                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1252                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1253                && matches!(
1254                    owner_segment,
1255                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1256                )
1257            {
1258                // segment is owned -> delete the owner
1259                resolved_segment_ids_to_delete.insert(owner_id);
1260            } else {
1261                // segment is not owned by anything -> can be deleted
1262                resolved_segment_ids_to_delete.insert(segment_id);
1263            }
1264        }
1265        let referenced_constraint_ids = self
1266            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1267            .map_err(KclErrorWithOutputs::no_outputs)?;
1268
1269        let mut new_ast = self.program.ast.clone();
1270
1271        for constraint_id in referenced_constraint_ids {
1272            if constraint_ids_set.contains(&constraint_id) {
1273                continue;
1274            }
1275
1276            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1277                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1278            })?;
1279            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1280                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1281                    "Object is not a constraint, it is {}",
1282                    constraint_object.kind.human_friendly_kind_with_article()
1283                ))));
1284            };
1285
1286            match constraint {
1287                Constraint::Coincident(coincident) => {
1288                    let remaining_segments =
1289                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1290
1291                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1292                    if remaining_segments.len() >= 2 {
1293                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1294                            .map_err(KclErrorWithOutputs::no_outputs)?;
1295                    } else {
1296                        constraint_ids_set.insert(constraint_id);
1297                    }
1298                }
1299                Constraint::EqualRadius(equal_radius) => {
1300                    let remaining_input = equal_radius
1301                        .input
1302                        .iter()
1303                        .copied()
1304                        .filter(|segment_id| {
1305                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1306                        })
1307                        .collect::<Vec<_>>();
1308
1309                    if remaining_input.len() >= 2 {
1310                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1311                            .map_err(KclErrorWithOutputs::no_outputs)?;
1312                    } else {
1313                        constraint_ids_set.insert(constraint_id);
1314                    }
1315                }
1316                Constraint::LinesEqualLength(lines_equal_length) => {
1317                    let remaining_lines = lines_equal_length
1318                        .lines
1319                        .iter()
1320                        .copied()
1321                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1322                        .collect::<Vec<_>>();
1323
1324                    // Equal length constraint is only valid with at least 2 lines
1325                    if remaining_lines.len() >= 2 {
1326                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1327                            .map_err(KclErrorWithOutputs::no_outputs)?;
1328                    } else {
1329                        constraint_ids_set.insert(constraint_id);
1330                    }
1331                }
1332                Constraint::Parallel(parallel) => {
1333                    let remaining_lines = parallel
1334                        .lines
1335                        .iter()
1336                        .copied()
1337                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1338                        .collect::<Vec<_>>();
1339
1340                    if remaining_lines.len() >= 2 {
1341                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1342                            .map_err(KclErrorWithOutputs::no_outputs)?;
1343                    } else {
1344                        constraint_ids_set.insert(constraint_id);
1345                    }
1346                }
1347                Constraint::Horizontal(Horizontal::Points { points }) => {
1348                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1349
1350                    if remaining_points.len() >= 2 {
1351                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1352                            .map_err(KclErrorWithOutputs::no_outputs)?;
1353                    } else {
1354                        constraint_ids_set.insert(constraint_id);
1355                    }
1356                }
1357                Constraint::Vertical(Vertical::Points { points }) => {
1358                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1359
1360                    if remaining_points.len() >= 2 {
1361                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1362                            .map_err(KclErrorWithOutputs::no_outputs)?;
1363                    } else {
1364                        constraint_ids_set.insert(constraint_id);
1365                    }
1366                }
1367                Constraint::Fixed(fixed) => {
1368                    if fixed.points.iter().any(|fixed_point| {
1369                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1370                    }) {
1371                        constraint_ids_set.insert(constraint_id);
1372                    }
1373                }
1374                _ => {
1375                    // All other constraint types: if referenced by a segment -> delete the constraint
1376                    constraint_ids_set.insert(constraint_id);
1377                }
1378            }
1379        }
1380
1381        for constraint_id in constraint_ids_set {
1382            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1383                .map_err(KclErrorWithOutputs::no_outputs)?;
1384        }
1385        for segment_id in resolved_segment_ids_to_delete {
1386            self.delete_segment(&mut new_ast, sketch, segment_id)
1387                .map_err(KclErrorWithOutputs::no_outputs)?;
1388        }
1389
1390        self.execute_after_edit(
1391            ctx,
1392            sketch,
1393            sketch_block_ref,
1394            &mut new_ast,
1395            ExecuteAfterEditOptions {
1396                segment_ids_edited: Default::default(),
1397                edit_kind: EditDeleteKind::DeleteNonSketch,
1398                commit_solved_initial_guesses: true,
1399            },
1400        )
1401        .await
1402    }
1403
1404    async fn add_constraint(
1405        &mut self,
1406        ctx: &ExecutorContext,
1407        _version: Version,
1408        sketch: ObjectId,
1409        constraint: Constraint,
1410    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1411        // TODO: Check version.
1412
1413        // Save the original state as a backup - we'll restore it if anything fails
1414        let original_program = self.program.clone();
1415        let original_scene_graph = self.scene_graph.clone();
1416
1417        let mut new_ast = self.program.ast.clone();
1418        let sketch_block_ref = match constraint {
1419            Constraint::Coincident(coincident) => self
1420                .add_coincident(sketch, coincident, &mut new_ast)
1421                .await
1422                .map_err(KclErrorWithOutputs::no_outputs)?,
1423            Constraint::Distance(distance) => self
1424                .add_distance(sketch, distance, &mut new_ast)
1425                .await
1426                .map_err(KclErrorWithOutputs::no_outputs)?,
1427            Constraint::EqualRadius(equal_radius) => self
1428                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1429                .await
1430                .map_err(KclErrorWithOutputs::no_outputs)?,
1431            Constraint::Fixed(fixed) => self
1432                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1433                .await
1434                .map_err(KclErrorWithOutputs::no_outputs)?,
1435            Constraint::HorizontalDistance(distance) => self
1436                .add_horizontal_distance(sketch, distance, &mut new_ast)
1437                .await
1438                .map_err(KclErrorWithOutputs::no_outputs)?,
1439            Constraint::VerticalDistance(distance) => self
1440                .add_vertical_distance(sketch, distance, &mut new_ast)
1441                .await
1442                .map_err(KclErrorWithOutputs::no_outputs)?,
1443            Constraint::Horizontal(horizontal) => self
1444                .add_horizontal(sketch, horizontal, &mut new_ast)
1445                .await
1446                .map_err(KclErrorWithOutputs::no_outputs)?,
1447            Constraint::LinesEqualLength(lines_equal_length) => self
1448                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1449                .await
1450                .map_err(KclErrorWithOutputs::no_outputs)?,
1451            Constraint::Midpoint(midpoint) => self
1452                .add_midpoint(sketch, midpoint, &mut new_ast)
1453                .await
1454                .map_err(KclErrorWithOutputs::no_outputs)?,
1455            Constraint::Parallel(parallel) => self
1456                .add_parallel(sketch, parallel, &mut new_ast)
1457                .await
1458                .map_err(KclErrorWithOutputs::no_outputs)?,
1459            Constraint::Perpendicular(perpendicular) => self
1460                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1461                .await
1462                .map_err(KclErrorWithOutputs::no_outputs)?,
1463            Constraint::Radius(radius) => self
1464                .add_radius(sketch, radius, &mut new_ast)
1465                .await
1466                .map_err(KclErrorWithOutputs::no_outputs)?,
1467            Constraint::Diameter(diameter) => self
1468                .add_diameter(sketch, diameter, &mut new_ast)
1469                .await
1470                .map_err(KclErrorWithOutputs::no_outputs)?,
1471            Constraint::Symmetric(symmetric) => self
1472                .add_symmetric(sketch, symmetric, &mut new_ast)
1473                .await
1474                .map_err(KclErrorWithOutputs::no_outputs)?,
1475            Constraint::Vertical(vertical) => self
1476                .add_vertical(sketch, vertical, &mut new_ast)
1477                .await
1478                .map_err(KclErrorWithOutputs::no_outputs)?,
1479            Constraint::Angle(lines_at_angle) => self
1480                .add_angle(sketch, lines_at_angle, &mut new_ast)
1481                .await
1482                .map_err(KclErrorWithOutputs::no_outputs)?,
1483            Constraint::Tangent(tangent) => self
1484                .add_tangent(sketch, tangent, &mut new_ast)
1485                .await
1486                .map_err(KclErrorWithOutputs::no_outputs)?,
1487        };
1488
1489        let result = self
1490            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1491            .await;
1492
1493        // If execution failed, restore the original state to prevent corruption
1494        if result.is_err() {
1495            self.program = original_program;
1496            self.scene_graph = original_scene_graph;
1497        }
1498
1499        result
1500    }
1501
1502    async fn chain_segment(
1503        &mut self,
1504        ctx: &ExecutorContext,
1505        version: Version,
1506        sketch: ObjectId,
1507        previous_segment_end_point_id: ObjectId,
1508        segment: SegmentCtor,
1509        _label: Option<String>,
1510    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1511        // TODO: Check version.
1512
1513        // First, add the segment (line) to get its start point ID
1514        let SegmentCtor::Line(line_ctor) = segment else {
1515            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1516                "chain_segment currently only supports Line segments, got {}",
1517                segment.human_friendly_kind_with_article(),
1518            ))));
1519        };
1520
1521        // Add the line segment first - this updates self.program and self.scene_graph
1522        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1523
1524        // Find the new line's start point ID from the updated scene graph
1525        // add_line updates self.scene_graph, so we can use that
1526        let new_line_id = first_scene_delta
1527            .new_objects
1528            .iter()
1529            .find(|&obj_id| {
1530                let obj = self.scene_graph.objects.get(obj_id.0);
1531                if let Some(obj) = obj {
1532                    matches!(
1533                        &obj.kind,
1534                        ObjectKind::Segment {
1535                            segment: Segment::Line(_)
1536                        }
1537                    )
1538                } else {
1539                    false
1540                }
1541            })
1542            .ok_or_else(|| {
1543                KclErrorWithOutputs::no_outputs(KclError::refactor(
1544                    "Failed to find new line segment in scene graph".to_string(),
1545                ))
1546            })?;
1547
1548        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1549            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1550                "New line object not found: {new_line_id:?}"
1551            )))
1552        })?;
1553
1554        let ObjectKind::Segment {
1555            segment: new_line_segment,
1556        } = &new_line_obj.kind
1557        else {
1558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559                "Object is not a segment: {new_line_obj:?}"
1560            ))));
1561        };
1562
1563        let Segment::Line(new_line) = new_line_segment else {
1564            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1565                "Segment is not a line: {new_line_segment:?}"
1566            ))));
1567        };
1568
1569        let new_line_start_point_id = new_line.start;
1570
1571        // Now add the coincident constraint between the previous end point and the new line's start point.
1572        let coincident = Coincident {
1573            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1574        };
1575
1576        let (final_src_delta, final_scene_delta) = self
1577            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1578            .await?;
1579
1580        // Combine new objects from the line addition and the constraint addition.
1581        // Both add_line and add_constraint now populate new_objects correctly.
1582        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1583        combined_new_objects.extend(final_scene_delta.new_objects);
1584
1585        let scene_graph_delta = SceneGraphDelta {
1586            new_graph: self.scene_graph_for_ui(),
1587            invalidates_ids: false,
1588            new_objects: combined_new_objects,
1589            exec_outcome: final_scene_delta.exec_outcome,
1590        };
1591
1592        Ok((final_src_delta, scene_graph_delta))
1593    }
1594
1595    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1596    async fn edit_constraint_value(
1597        &mut self,
1598        ctx: &ExecutorContext,
1599        _version: Version,
1600        sketch: ObjectId,
1601        constraint_id: ObjectId,
1602        value_expression: String,
1603    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1604        // TODO: Check version.
1605        let sketch_block_ref =
1606            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1607
1608        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1609            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1610        })?;
1611        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1612            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1613                "Object is not a constraint: {constraint_id:?}"
1614            ))));
1615        }
1616
1617        let mut new_ast = self.program.ast.clone();
1618
1619        // Parse the expression string into an AST node.
1620        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1621            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1622                "Invalid constraint value",
1623                &e,
1624            )))
1625        })?;
1626        if !errors.is_empty() {
1627            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1628                format_compilation_issues("Invalid constraint value", &errors),
1629            )));
1630        }
1631        let mut parsed = parsed.ok_or_else(|| {
1632            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1633        })?;
1634        if parsed.ast.body.is_empty() {
1635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636                "Empty value expression".to_string(),
1637            )));
1638        }
1639        let first = parsed.ast.body.remove(0);
1640        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1641            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1642                "Value expression must be a simple expression".to_string(),
1643            )));
1644        };
1645
1646        let new_value: ast::BinaryPart = expr_stmt
1647            .inner
1648            .expression
1649            .try_into()
1650            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1651
1652        self.mutate_ast(
1653            &mut new_ast,
1654            constraint_id,
1655            AstMutateCommand::EditConstraintValue { value: new_value },
1656        )
1657        .map_err(KclErrorWithOutputs::no_outputs)?;
1658
1659        self.execute_after_edit(
1660            ctx,
1661            sketch,
1662            sketch_block_ref,
1663            &mut new_ast,
1664            ExecuteAfterEditOptions {
1665                segment_ids_edited: Default::default(),
1666                edit_kind: EditDeleteKind::Edit,
1667                commit_solved_initial_guesses: true,
1668            },
1669        )
1670        .await
1671    }
1672
1673    async fn edit_distance_constraint_label_position(
1674        &mut self,
1675        ctx: &ExecutorContext,
1676        _version: Version,
1677        sketch: ObjectId,
1678        constraint_id: ObjectId,
1679        label_position: Point2d<Number>,
1680        anchor_segment_ids: Vec<ObjectId>,
1681    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1682        // TODO: Check version.
1683        let sketch_block_ref =
1684            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1685
1686        let mut new_ast = self.program.ast.clone();
1687        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1688            .map_err(KclErrorWithOutputs::no_outputs)?;
1689        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1690
1691        self.execute_after_edit(
1692            ctx,
1693            sketch,
1694            sketch_block_ref,
1695            &mut new_ast,
1696            ExecuteAfterEditOptions {
1697                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1698                edit_kind: EditDeleteKind::Edit,
1699                commit_solved_initial_guesses,
1700            },
1701        )
1702        .await
1703    }
1704
1705    /// Splitting a segment means creating a new segment, editing the old one, and then
1706    /// migrating a bunch of the constraints from the original segment to the new one
1707    /// (i.e. deleting them and re-adding them on the other segment).
1708    ///
1709    /// To keep this efficient we require as few executions as possible: we create the
1710    /// new segment first (to get its id), then do all edits and new constraints, and
1711    /// do all deletes at the end (since deletes invalidate ids).
1712    async fn batch_split_segment_operations(
1713        &mut self,
1714        ctx: &ExecutorContext,
1715        _version: Version,
1716        sketch: ObjectId,
1717        edit_segments: Vec<ExistingSegmentCtor>,
1718        add_constraints: Vec<Constraint>,
1719        delete_constraint_ids: Vec<ObjectId>,
1720        _new_segment_info: sketch::NewSegmentInfo,
1721    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1722        // TODO: Check version.
1723        let sketch_block_ref =
1724            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1725
1726        let mut new_ast = self.program.ast.clone();
1727        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1728
1729        // Step 1: Edit segments
1730        for segment in edit_segments {
1731            segment_ids_edited.insert(segment.id);
1732            match segment.ctor {
1733                SegmentCtor::Point(ctor) => self
1734                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1735                    .map_err(KclErrorWithOutputs::no_outputs)?,
1736                SegmentCtor::Line(ctor) => self
1737                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1738                    .map_err(KclErrorWithOutputs::no_outputs)?,
1739                SegmentCtor::Arc(ctor) => self
1740                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1741                    .map_err(KclErrorWithOutputs::no_outputs)?,
1742                SegmentCtor::Circle(ctor) => self
1743                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1744                    .map_err(KclErrorWithOutputs::no_outputs)?,
1745                SegmentCtor::ControlPointSpline(ctor) => self
1746                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1747                    .map_err(KclErrorWithOutputs::no_outputs)?,
1748            }
1749        }
1750
1751        // Step 2: Add all constraints
1752        for constraint in add_constraints {
1753            match constraint {
1754                Constraint::Coincident(coincident) => {
1755                    self.add_coincident(sketch, coincident, &mut new_ast)
1756                        .await
1757                        .map_err(KclErrorWithOutputs::no_outputs)?;
1758                }
1759                Constraint::Distance(distance) => {
1760                    self.add_distance(sketch, distance, &mut new_ast)
1761                        .await
1762                        .map_err(KclErrorWithOutputs::no_outputs)?;
1763                }
1764                Constraint::EqualRadius(equal_radius) => {
1765                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1766                        .await
1767                        .map_err(KclErrorWithOutputs::no_outputs)?;
1768                }
1769                Constraint::Fixed(fixed) => {
1770                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1771                        .await
1772                        .map_err(KclErrorWithOutputs::no_outputs)?;
1773                }
1774                Constraint::HorizontalDistance(distance) => {
1775                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1776                        .await
1777                        .map_err(KclErrorWithOutputs::no_outputs)?;
1778                }
1779                Constraint::VerticalDistance(distance) => {
1780                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1781                        .await
1782                        .map_err(KclErrorWithOutputs::no_outputs)?;
1783                }
1784                Constraint::Horizontal(horizontal) => {
1785                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1786                        .await
1787                        .map_err(KclErrorWithOutputs::no_outputs)?;
1788                }
1789                Constraint::LinesEqualLength(lines_equal_length) => {
1790                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1791                        .await
1792                        .map_err(KclErrorWithOutputs::no_outputs)?;
1793                }
1794                Constraint::Midpoint(midpoint) => {
1795                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1796                        .await
1797                        .map_err(KclErrorWithOutputs::no_outputs)?;
1798                }
1799                Constraint::Parallel(parallel) => {
1800                    self.add_parallel(sketch, parallel, &mut new_ast)
1801                        .await
1802                        .map_err(KclErrorWithOutputs::no_outputs)?;
1803                }
1804                Constraint::Perpendicular(perpendicular) => {
1805                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1806                        .await
1807                        .map_err(KclErrorWithOutputs::no_outputs)?;
1808                }
1809                Constraint::Vertical(vertical) => {
1810                    self.add_vertical(sketch, vertical, &mut new_ast)
1811                        .await
1812                        .map_err(KclErrorWithOutputs::no_outputs)?;
1813                }
1814                Constraint::Diameter(diameter) => {
1815                    self.add_diameter(sketch, diameter, &mut new_ast)
1816                        .await
1817                        .map_err(KclErrorWithOutputs::no_outputs)?;
1818                }
1819                Constraint::Radius(radius) => {
1820                    self.add_radius(sketch, radius, &mut new_ast)
1821                        .await
1822                        .map_err(KclErrorWithOutputs::no_outputs)?;
1823                }
1824                Constraint::Symmetric(symmetric) => {
1825                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1826                        .await
1827                        .map_err(KclErrorWithOutputs::no_outputs)?;
1828                }
1829                Constraint::Angle(angle) => {
1830                    self.add_angle(sketch, angle, &mut new_ast)
1831                        .await
1832                        .map_err(KclErrorWithOutputs::no_outputs)?;
1833                }
1834                Constraint::Tangent(tangent) => {
1835                    self.add_tangent(sketch, tangent, &mut new_ast)
1836                        .await
1837                        .map_err(KclErrorWithOutputs::no_outputs)?;
1838                }
1839            }
1840        }
1841
1842        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1843        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1844
1845        let has_constraint_deletions = !constraint_ids_set.is_empty();
1846        for constraint_id in constraint_ids_set {
1847            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1848                .map_err(KclErrorWithOutputs::no_outputs)?;
1849        }
1850
1851        // Step 4: Execute once at the end
1852        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1853        // But we'll manually set invalidates_ids: true if we deleted constraints
1854        let (source_delta, mut scene_graph_delta) = self
1855            .execute_after_edit(
1856                ctx,
1857                sketch,
1858                sketch_block_ref,
1859                &mut new_ast,
1860                ExecuteAfterEditOptions {
1861                    segment_ids_edited,
1862                    edit_kind: EditDeleteKind::Edit,
1863                    commit_solved_initial_guesses: true,
1864                },
1865            )
1866            .await?;
1867
1868        // If we deleted constraints, set invalidates_ids: true
1869        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1870        if has_constraint_deletions {
1871            scene_graph_delta.invalidates_ids = true;
1872        }
1873
1874        Ok((source_delta, scene_graph_delta))
1875    }
1876
1877    async fn batch_tail_cut_operations(
1878        &mut self,
1879        ctx: &ExecutorContext,
1880        _version: Version,
1881        sketch: ObjectId,
1882        edit_segments: Vec<ExistingSegmentCtor>,
1883        add_constraints: Vec<Constraint>,
1884        delete_constraint_ids: Vec<ObjectId>,
1885        additional_edited_segment_ids: Vec<ObjectId>,
1886    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1887        let sketch_block_ref =
1888            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1889
1890        let mut new_ast = self.program.ast.clone();
1891        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1892
1893        // Step 1: Edit segments (usually a single segment for tail cut)
1894        for segment in edit_segments {
1895            segment_ids_edited.insert(segment.id);
1896            match segment.ctor {
1897                SegmentCtor::Point(ctor) => self
1898                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1899                    .map_err(KclErrorWithOutputs::no_outputs)?,
1900                SegmentCtor::Line(ctor) => self
1901                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1902                    .map_err(KclErrorWithOutputs::no_outputs)?,
1903                SegmentCtor::Arc(ctor) => self
1904                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1905                    .map_err(KclErrorWithOutputs::no_outputs)?,
1906                SegmentCtor::Circle(ctor) => self
1907                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1908                    .map_err(KclErrorWithOutputs::no_outputs)?,
1909                SegmentCtor::ControlPointSpline(ctor) => self
1910                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1911                    .map_err(KclErrorWithOutputs::no_outputs)?,
1912            }
1913        }
1914
1915        segment_ids_edited.extend(additional_edited_segment_ids);
1916
1917        // Step 2: Add coincident constraints
1918        for constraint in add_constraints {
1919            match constraint {
1920                Constraint::Coincident(coincident) => {
1921                    self.add_coincident(sketch, coincident, &mut new_ast)
1922                        .await
1923                        .map_err(KclErrorWithOutputs::no_outputs)?;
1924                }
1925                other => {
1926                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1927                        "unsupported constraint in tail cut batch: {other:?}"
1928                    ))));
1929                }
1930            }
1931        }
1932
1933        // Step 3: Delete constraints (if any)
1934        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1935
1936        let has_constraint_deletions = !constraint_ids_set.is_empty();
1937        for constraint_id in constraint_ids_set {
1938            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1939                .map_err(KclErrorWithOutputs::no_outputs)?;
1940        }
1941
1942        // Step 4: Single execute_after_edit
1943        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1944        // But we'll manually set invalidates_ids: true if we deleted constraints
1945        let (source_delta, mut scene_graph_delta) = self
1946            .execute_after_edit(
1947                ctx,
1948                sketch,
1949                sketch_block_ref,
1950                &mut new_ast,
1951                ExecuteAfterEditOptions {
1952                    segment_ids_edited,
1953                    edit_kind: EditDeleteKind::Edit,
1954                    commit_solved_initial_guesses: true,
1955                },
1956            )
1957            .await?;
1958
1959        // If we deleted constraints, set invalidates_ids: true
1960        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1961        if has_constraint_deletions {
1962            scene_graph_delta.invalidates_ids = true;
1963        }
1964
1965        Ok((source_delta, scene_graph_delta))
1966    }
1967}
1968
1969impl FrontendState {
1970    /// Evaluate an input expression using the current model's settings and cached variables.
1971    pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1972        program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1973        ctx.run_mock(&program, &MockConfig::default()).await
1974    }
1975
1976    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1977        self.program = program.clone();
1978
1979        // Execute so that the objects are updated and available for the next
1980        // API call.
1981        // This always uses engine execution (not mock) so that things are cached.
1982        // Engine execution now runs freedom analysis automatically.
1983        // Keep existing checkpoints alive here. History may still reference
1984        // older committed sketch states across a direct-edit boundary, and a
1985        // checkpoint restore is a full state replacement anyway. We append a
1986        // fresh baseline checkpoint after the full execution below.
1987        // Clear the freedom cache since IDs might have changed after direct editing
1988        // and we're about to run freedom analysis which will repopulate it.
1989        self.point_freedom_cache.clear();
1990        match ctx.run_with_caching(program).await {
1991            Ok(outcome) => {
1992                let outcome = self.update_state_after_exec(outcome, true);
1993                let checkpoint_id = self
1994                    .create_sketch_checkpoint(&ctx.cache, outcome.clone())
1995                    .await
1996                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1997                Ok(SetProgramOutcome::Success {
1998                    scene_graph: Box::new(self.scene_graph_for_ui()),
1999                    exec_outcome: Box::new(outcome),
2000                    checkpoint_id: Some(checkpoint_id),
2001                })
2002            }
2003            Err(mut err) => {
2004                // Don't return an error just because execution failed. Instead,
2005                // update state as much as possible.
2006                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2007                self.update_state_after_exec(outcome, true);
2008                err.scene_graph = Some(self.scene_graph_for_ui());
2009                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2010            }
2011        }
2012    }
2013
2014    /// Decorate engine execution such that our state is updated and the scene
2015    /// graph is added to the return.
2016    pub async fn engine_execute(
2017        &mut self,
2018        ctx: &ExecutorContext,
2019        program: Program,
2020    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2021        self.program = program.clone();
2022
2023        // Engine execution now runs freedom analysis automatically. Clear the
2024        // freedom cache since IDs might have changed after direct editing, and
2025        // we're about to run freedom analysis which will repopulate it.
2026        self.point_freedom_cache.clear();
2027        match ctx.run_with_caching(program).await {
2028            Ok(outcome) => {
2029                let outcome = self.update_state_after_exec(outcome, true);
2030                Ok(SceneGraphDelta {
2031                    new_graph: self.scene_graph_for_ui(),
2032                    exec_outcome: outcome,
2033                    // We don't know what the new objects are.
2034                    new_objects: Default::default(),
2035                    // We don't know if IDs were invalidated.
2036                    invalidates_ids: Default::default(),
2037                })
2038            }
2039            Err(mut err) => {
2040                // Update state as much as possible, even when there's an error.
2041                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2042                self.update_state_after_exec(outcome, true);
2043                err.scene_graph = Some(self.scene_graph_for_ui());
2044                Err(err)
2045            }
2046        }
2047    }
2048
2049    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2050        if matches!(err.error, KclError::EngineHangup { .. }) {
2051            // It's not ideal to special-case this, but this error is very
2052            // common during development, and it causes confusing downstream
2053            // errors that have nothing to do with the actual problem.
2054            return Err(err);
2055        }
2056
2057        let KclErrorWithOutputs {
2058            error,
2059            mut non_fatal,
2060            variables,
2061            operations,
2062            artifact_graph,
2063            scene_objects,
2064            source_range_to_object,
2065            var_solutions,
2066            refactor_metadata,
2067            filenames,
2068            source_files,
2069            default_planes,
2070            ..
2071        } = err;
2072
2073        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2074
2075        Ok(ExecOutcome {
2076            variables,
2077            #[cfg(test)]
2078            test_program_memory: Default::default(),
2079            filenames,
2080            from_cache: false,
2081            operations,
2082            artifact_graph,
2083            scene_objects,
2084            source_range_to_object,
2085            var_solutions,
2086            refactor_metadata,
2087            issues: non_fatal,
2088            source_files,
2089            default_planes,
2090        })
2091    }
2092
2093    async fn add_point(
2094        &mut self,
2095        ctx: &ExecutorContext,
2096        sketch: ObjectId,
2097        ctor: PointCtor,
2098    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2099        // Create updated KCL source from args.
2100        let at_ast = to_ast_point2d(&ctor.position)
2101            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2102        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2103            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2104            unlabeled: None,
2105            arguments: vec![ast::LabeledArg {
2106                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2107                arg: at_ast,
2108            }],
2109            digest: None,
2110            non_code_meta: Default::default(),
2111        })));
2112
2113        // Look up existing sketch.
2114        let sketch_id = sketch;
2115        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2116            #[cfg(target_arch = "wasm32")]
2117            web_sys::console::error_1(
2118                &format!(
2119                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2120                    self.scene_graph.objects
2121                )
2122                .into(),
2123            );
2124            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2125        })?;
2126        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2127            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2128                "Object is not a sketch, it is {}",
2129                sketch_object.kind.human_friendly_kind_with_article(),
2130            ))));
2131        };
2132        // Add the point to the AST of the sketch block.
2133        let mut new_ast = self.program.ast.clone();
2134        let (sketch_block_ref, _) = self
2135            .mutate_ast(
2136                &mut new_ast,
2137                sketch_id,
2138                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2139            )
2140            .map_err(KclErrorWithOutputs::no_outputs)?;
2141        // Convert to string source to create real source ranges.
2142        let new_source = source_from_ast(&new_ast);
2143        // Parse the new KCL source.
2144        let new_program = parse_frontend_mutation_source(
2145            &new_source,
2146            "Error parsing KCL source after adding point",
2147            "No AST produced after adding point",
2148        )?;
2149
2150        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2151            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2152                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2153            )))
2154        })?;
2155
2156        // Make sure to only set this if there are no errors.
2157        self.program = new_program.clone();
2158
2159        // Truncate after the sketch block for mock execution.
2160        let mut truncated_program = new_program;
2161        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2162            .map_err(KclErrorWithOutputs::no_outputs)?;
2163
2164        // Execute.
2165        let outcome = ctx
2166            .run_mock(
2167                &truncated_program,
2168                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2169            )
2170            .await?;
2171
2172        let new_object_ids = {
2173            let make_err =
2174                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2175            let segment_id = outcome
2176                .source_range_to_object
2177                .get(&point_node_ref.range)
2178                .copied()
2179                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2180            let segment_object = outcome
2181                .scene_objects
2182                .get(segment_id.0)
2183                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2184            let ObjectKind::Segment { segment } = &segment_object.kind else {
2185                return Err(make_err(format!(
2186                    "Object is not a segment, it is {}",
2187                    segment_object.kind.human_friendly_kind_with_article()
2188                )));
2189            };
2190            let Segment::Point(_) = segment else {
2191                return Err(make_err(format!(
2192                    "Segment is not a point, it is {}",
2193                    segment.human_friendly_kind_with_article()
2194                )));
2195            };
2196            vec![segment_id]
2197        };
2198        let src_delta = SourceDelta { text: new_source };
2199        // Uses .no_freedom_analysis() so freedom_analysis: false
2200        let outcome = self.update_state_after_exec(outcome, false);
2201        let scene_graph_delta = SceneGraphDelta {
2202            new_graph: self.scene_graph_for_ui(),
2203            invalidates_ids: false,
2204            new_objects: new_object_ids,
2205            exec_outcome: outcome,
2206        };
2207        Ok((src_delta, scene_graph_delta))
2208    }
2209
2210    async fn add_line(
2211        &mut self,
2212        ctx: &ExecutorContext,
2213        sketch: ObjectId,
2214        ctor: LineCtor,
2215    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2216        // Create updated KCL source from args.
2217        let start_ast = to_ast_point2d(&ctor.start)
2218            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219        let end_ast = to_ast_point2d(&ctor.end)
2220            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221        let mut arguments = vec![
2222            ast::LabeledArg {
2223                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2224                arg: start_ast,
2225            },
2226            ast::LabeledArg {
2227                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2228                arg: end_ast,
2229            },
2230        ];
2231        // Add construction kwarg if construction is Some(true)
2232        if ctor.construction == Some(true) {
2233            arguments.push(ast::LabeledArg {
2234                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2235                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2236                    value: ast::LiteralValue::Bool(true),
2237                    raw: "true".to_string(),
2238                    digest: None,
2239                }))),
2240            });
2241        }
2242        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2243            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2244            unlabeled: None,
2245            arguments,
2246            digest: None,
2247            non_code_meta: Default::default(),
2248        })));
2249
2250        // Look up existing sketch.
2251        let sketch_id = sketch;
2252        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2253            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2254        })?;
2255        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2256            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2257                "Object is not a sketch, it is {}",
2258                sketch_object.kind.human_friendly_kind_with_article(),
2259            ))));
2260        };
2261        // Add the line to the AST of the sketch block.
2262        let mut new_ast = self.program.ast.clone();
2263        let (sketch_block_ref, _) = self
2264            .mutate_ast(
2265                &mut new_ast,
2266                sketch_id,
2267                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2268            )
2269            .map_err(KclErrorWithOutputs::no_outputs)?;
2270        // Convert to string source to create real source ranges.
2271        let new_source = source_from_ast(&new_ast);
2272        // Parse the new KCL source.
2273        let new_program = parse_frontend_mutation_source(
2274            &new_source,
2275            "Error parsing KCL source after adding line",
2276            "No AST produced after adding line",
2277        )?;
2278
2279        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2280            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2281                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2282            )))
2283        })?;
2284
2285        // Make sure to only set this if there are no errors.
2286        self.program = new_program.clone();
2287
2288        // Truncate after the sketch block for mock execution.
2289        let mut truncated_program = new_program;
2290        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2291            .map_err(KclErrorWithOutputs::no_outputs)?;
2292
2293        // Execute.
2294        let outcome = ctx
2295            .run_mock(
2296                &truncated_program,
2297                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2298            )
2299            .await?;
2300
2301        let new_object_ids = {
2302            let make_err =
2303                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2304            let segment_id = outcome
2305                .source_range_to_object
2306                .get(&line_node_ref.range)
2307                .copied()
2308                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2309            let segment_object = outcome
2310                .scene_object_by_id(segment_id)
2311                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2312            let ObjectKind::Segment { segment } = &segment_object.kind else {
2313                return Err(make_err(format!(
2314                    "Object is not a segment, it is {}",
2315                    segment_object.kind.human_friendly_kind_with_article()
2316                )));
2317            };
2318            let Segment::Line(line) = segment else {
2319                return Err(make_err(format!(
2320                    "Segment is not a line, it is {}",
2321                    segment.human_friendly_kind_with_article()
2322                )));
2323            };
2324            vec![line.start, line.end, segment_id]
2325        };
2326        let src_delta = SourceDelta { text: new_source };
2327        // Uses .no_freedom_analysis() so freedom_analysis: false
2328        let outcome = self.update_state_after_exec(outcome, false);
2329        let scene_graph_delta = SceneGraphDelta {
2330            new_graph: self.scene_graph_for_ui(),
2331            invalidates_ids: false,
2332            new_objects: new_object_ids,
2333            exec_outcome: outcome,
2334        };
2335        Ok((src_delta, scene_graph_delta))
2336    }
2337
2338    async fn add_arc(
2339        &mut self,
2340        ctx: &ExecutorContext,
2341        sketch: ObjectId,
2342        ctor: ArcCtor,
2343    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2344        // Create updated KCL source from args.
2345        let start_ast = to_ast_point2d(&ctor.start)
2346            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2347        let end_ast = to_ast_point2d(&ctor.end)
2348            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2349        let center_ast = to_ast_point2d(&ctor.center)
2350            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2351        let mut arguments = vec![
2352            ast::LabeledArg {
2353                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2354                arg: start_ast,
2355            },
2356            ast::LabeledArg {
2357                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2358                arg: end_ast,
2359            },
2360            ast::LabeledArg {
2361                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2362                arg: center_ast,
2363            },
2364        ];
2365        // Add direction kwarg if it's clockwise, since counterclockwise is the
2366        // default.
2367        if ctor.direction == Some(ArcDirection::Cw) {
2368            arguments.push(ast::LabeledArg {
2369                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2370                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2371            });
2372        }
2373        // Add construction kwarg if construction is Some(true)
2374        if ctor.construction == Some(true) {
2375            arguments.push(ast::LabeledArg {
2376                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2377                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2378                    value: ast::LiteralValue::Bool(true),
2379                    raw: "true".to_string(),
2380                    digest: None,
2381                }))),
2382            });
2383        }
2384        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2385            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2386            unlabeled: None,
2387            arguments,
2388            digest: None,
2389            non_code_meta: Default::default(),
2390        })));
2391
2392        // Look up existing sketch.
2393        let sketch_id = sketch;
2394        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2395            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2396        })?;
2397        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2398            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2399                "Object is not a sketch, it is {}",
2400                sketch_object.kind.human_friendly_kind_with_article(),
2401            ))));
2402        };
2403        // Add the arc to the AST of the sketch block.
2404        let mut new_ast = self.program.ast.clone();
2405        let (sketch_block_ref, _) = self
2406            .mutate_ast(
2407                &mut new_ast,
2408                sketch_id,
2409                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2410            )
2411            .map_err(KclErrorWithOutputs::no_outputs)?;
2412        // Convert to string source to create real source ranges.
2413        let new_source = source_from_ast(&new_ast);
2414        // Parse the new KCL source.
2415        let new_program = parse_frontend_mutation_source(
2416            &new_source,
2417            "Error parsing KCL source after adding arc",
2418            "No AST produced after adding arc",
2419        )?;
2420
2421        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2422            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2423                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2424            )))
2425        })?;
2426
2427        // Make sure to only set this if there are no errors.
2428        self.program = new_program.clone();
2429
2430        // Truncate after the sketch block for mock execution.
2431        let mut truncated_program = new_program;
2432        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2433            .map_err(KclErrorWithOutputs::no_outputs)?;
2434
2435        // Execute.
2436        let outcome = ctx
2437            .run_mock(
2438                &truncated_program,
2439                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2440            )
2441            .await?;
2442
2443        let new_object_ids = {
2444            let make_err =
2445                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2446            let segment_id = outcome
2447                .source_range_to_object
2448                .get(&arc_node_ref.range)
2449                .copied()
2450                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2451            let segment_object = outcome
2452                .scene_objects
2453                .get(segment_id.0)
2454                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2455            let ObjectKind::Segment { segment } = &segment_object.kind else {
2456                return Err(make_err(format!(
2457                    "Object is not a segment, it is {}",
2458                    segment_object.kind.human_friendly_kind_with_article()
2459                )));
2460            };
2461            let Segment::Arc(arc) = segment else {
2462                return Err(make_err(format!(
2463                    "Segment is not an arc, it is {}",
2464                    segment.human_friendly_kind_with_article()
2465                )));
2466            };
2467            vec![arc.start, arc.end, arc.center, segment_id]
2468        };
2469        let src_delta = SourceDelta { text: new_source };
2470        // Uses .no_freedom_analysis() so freedom_analysis: false
2471        let outcome = self.update_state_after_exec(outcome, false);
2472        let scene_graph_delta = SceneGraphDelta {
2473            new_graph: self.scene_graph_for_ui(),
2474            invalidates_ids: false,
2475            new_objects: new_object_ids,
2476            exec_outcome: outcome,
2477        };
2478        Ok((src_delta, scene_graph_delta))
2479    }
2480
2481    async fn add_circle(
2482        &mut self,
2483        ctx: &ExecutorContext,
2484        sketch: ObjectId,
2485        ctor: CircleCtor,
2486    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2487        // Create updated KCL source from args.
2488        let start_ast = to_ast_point2d(&ctor.start)
2489            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2490        let center_ast = to_ast_point2d(&ctor.center)
2491            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2492        let mut arguments = vec![
2493            ast::LabeledArg {
2494                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2495                arg: start_ast,
2496            },
2497            ast::LabeledArg {
2498                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2499                arg: center_ast,
2500            },
2501        ];
2502        // Add construction kwarg if construction is Some(true)
2503        if ctor.construction == Some(true) {
2504            arguments.push(ast::LabeledArg {
2505                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2506                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2507                    value: ast::LiteralValue::Bool(true),
2508                    raw: "true".to_string(),
2509                    digest: None,
2510                }))),
2511            });
2512        }
2513        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2514            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2515            unlabeled: None,
2516            arguments,
2517            digest: None,
2518            non_code_meta: Default::default(),
2519        })));
2520
2521        // Look up existing sketch.
2522        let sketch_id = sketch;
2523        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2524            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2525        })?;
2526        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2527            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2528                "Object is not a sketch, it is {}",
2529                sketch_object.kind.human_friendly_kind_with_article(),
2530            ))));
2531        };
2532        // Add the circle to the AST of the sketch block.
2533        let mut new_ast = self.program.ast.clone();
2534        let (sketch_block_ref, _) = self
2535            .mutate_ast(
2536                &mut new_ast,
2537                sketch_id,
2538                AstMutateCommand::AddSketchBlockVarDecl {
2539                    prefix: CIRCLE_VARIABLE.to_owned(),
2540                    expr: circle_ast,
2541                },
2542            )
2543            .map_err(KclErrorWithOutputs::no_outputs)?;
2544        // Convert to string source to create real source ranges.
2545        let new_source = source_from_ast(&new_ast);
2546        // Parse the new KCL source.
2547        let new_program = parse_frontend_mutation_source(
2548            &new_source,
2549            "Error parsing KCL source after adding circle",
2550            "No AST produced after adding circle",
2551        )?;
2552
2553        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2554            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2555                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2556            )))
2557        })?;
2558
2559        // Make sure to only set this if there are no errors.
2560        self.program = new_program.clone();
2561
2562        // Truncate after the sketch block for mock execution.
2563        let mut truncated_program = new_program;
2564        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2565            .map_err(KclErrorWithOutputs::no_outputs)?;
2566
2567        // Execute.
2568        let outcome = ctx
2569            .run_mock(
2570                &truncated_program,
2571                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2572            )
2573            .await?;
2574
2575        let new_object_ids = {
2576            let make_err =
2577                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2578            let segment_id = outcome
2579                .source_range_to_object
2580                .get(&circle_node_ref.range)
2581                .copied()
2582                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2583            let segment_object = outcome
2584                .scene_objects
2585                .get(segment_id.0)
2586                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2587            let ObjectKind::Segment { segment } = &segment_object.kind else {
2588                return Err(make_err(format!(
2589                    "Object is not a segment, it is {}",
2590                    segment_object.kind.human_friendly_kind_with_article()
2591                )));
2592            };
2593            let Segment::Circle(circle) = segment else {
2594                return Err(make_err(format!(
2595                    "Segment is not a circle, it is {}",
2596                    segment.human_friendly_kind_with_article()
2597                )));
2598            };
2599            vec![circle.start, circle.center, segment_id]
2600        };
2601        let src_delta = SourceDelta { text: new_source };
2602        // Uses .no_freedom_analysis() so freedom_analysis: false
2603        let outcome = self.update_state_after_exec(outcome, false);
2604        let scene_graph_delta = SceneGraphDelta {
2605            new_graph: self.scene_graph_for_ui(),
2606            invalidates_ids: false,
2607            new_objects: new_object_ids,
2608            exec_outcome: outcome,
2609        };
2610        Ok((src_delta, scene_graph_delta))
2611    }
2612
2613    async fn add_control_point_spline(
2614        &mut self,
2615        ctx: &ExecutorContext,
2616        sketch: ObjectId,
2617        ctor: ControlPointSplineCtor,
2618    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2619        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2620            .map_err(KclErrorWithOutputs::no_outputs)?;
2621
2622        let points_ast = to_ast_point2d_array(&ctor.points)
2623            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2624        let mut arguments = vec![ast::LabeledArg {
2625            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2626            arg: points_ast,
2627        }];
2628        if ctor.construction == Some(true) {
2629            arguments.push(ast::LabeledArg {
2630                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2631                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2632                    value: ast::LiteralValue::Bool(true),
2633                    raw: "true".to_string(),
2634                    digest: None,
2635                }))),
2636            });
2637        }
2638        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2639            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2640            unlabeled: None,
2641            arguments,
2642            digest: None,
2643            non_code_meta: Default::default(),
2644        })));
2645
2646        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2647            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2648        })?;
2649        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2650            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2651                "Object is not a sketch, it is {}",
2652                sketch_object.kind.human_friendly_kind_with_article(),
2653            ))));
2654        };
2655
2656        let mut new_ast = new_program.ast.clone();
2657        let (sketch_block_ref, _) = self
2658            .mutate_ast(
2659                &mut new_ast,
2660                sketch,
2661                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2662            )
2663            .map_err(KclErrorWithOutputs::no_outputs)?;
2664        let new_source = source_from_ast(&new_ast);
2665        let new_program = parse_frontend_mutation_source(
2666            &new_source,
2667            "Error parsing KCL source after adding controlPointSpline",
2668            "No AST produced after adding controlPointSpline",
2669        )?;
2670
2671        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2672            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2673                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2674            )))
2675        })?;
2676
2677        self.program = new_program.clone();
2678
2679        let mut truncated_program = new_program;
2680        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2681            .map_err(KclErrorWithOutputs::no_outputs)?;
2682
2683        let outcome = ctx
2684            .run_mock(
2685                &truncated_program,
2686                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2687            )
2688            .await?;
2689
2690        let new_object_ids = {
2691            let make_err =
2692                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2693            let segment_id = outcome
2694                .source_range_to_object
2695                .get(&spline_node_ref.range)
2696                .copied()
2697                .ok_or_else(|| {
2698                    make_err(format!(
2699                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2700                    ))
2701                })?;
2702            let segment_object = outcome
2703                .scene_objects
2704                .get(segment_id.0)
2705                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2706            let ObjectKind::Segment { segment } = &segment_object.kind else {
2707                return Err(make_err(format!(
2708                    "Object is not a segment, it is {}",
2709                    segment_object.kind.human_friendly_kind_with_article()
2710                )));
2711            };
2712            let Segment::ControlPointSpline(spline) = segment else {
2713                return Err(make_err(format!(
2714                    "Segment is not a control point spline, it is {}",
2715                    segment.human_friendly_kind_with_article()
2716                )));
2717            };
2718
2719            let mut ids = outcome
2720                .scene_objects
2721                .iter()
2722                .filter_map(|obj| match &obj.kind {
2723                    ObjectKind::Segment {
2724                        segment: Segment::Line(line),
2725                    } if line.owner == Some(segment_id) => Some(obj.id),
2726                    _ => None,
2727                })
2728                .collect::<Vec<_>>();
2729            ids.extend(spline.controls.clone());
2730            ids.push(segment_id);
2731            ids
2732        };
2733        let src_delta = SourceDelta { text: new_source };
2734        let outcome = self.update_state_after_exec(outcome, false);
2735        let scene_graph_delta = SceneGraphDelta {
2736            new_graph: self.scene_graph_for_ui(),
2737            invalidates_ids: false,
2738            new_objects: new_object_ids,
2739            exec_outcome: outcome,
2740        };
2741        Ok((src_delta, scene_graph_delta))
2742    }
2743
2744    fn edit_point(
2745        &mut self,
2746        new_ast: &mut ast::Node<ast::Program>,
2747        sketch: ObjectId,
2748        point: ObjectId,
2749        ctor: PointCtor,
2750    ) -> Result<(), KclError> {
2751        // Create updated KCL source from args.
2752        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2753
2754        // Look up existing sketch.
2755        let sketch_id = sketch;
2756        let sketch_object = self
2757            .scene_graph
2758            .objects
2759            .get(sketch_id.0)
2760            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2761        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2762            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2763        };
2764        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2765            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2766        })?;
2767        // Look up existing point.
2768        let point_id = point;
2769        let point_object = self
2770            .scene_graph
2771            .objects
2772            .get(point_id.0)
2773            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2774        let ObjectKind::Segment {
2775            segment: Segment::Point(point),
2776        } = &point_object.kind
2777        else {
2778            return Err(KclError::refactor(format!(
2779                "Object is not a point segment: {point_object:?}"
2780            )));
2781        };
2782
2783        // If the point is part of a line or arc, edit the line/arc instead.
2784        if let Some(owner_id) = point.owner {
2785            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2786                KclError::refactor(format!(
2787                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2788                ))
2789            })?;
2790            let ObjectKind::Segment { segment } = &owner_object.kind else {
2791                return Err(KclError::refactor(format!(
2792                    "Internal: Owner of point is not a segment, but found {}",
2793                    owner_object.kind.human_friendly_kind_with_article()
2794                )));
2795            };
2796
2797            // Handle Line owner
2798            if let Segment::Line(line) = segment {
2799                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2800                    return Err(KclError::refactor(format!(
2801                        "Internal: Owner of point does not have line ctor, but found {}",
2802                        line.ctor.human_friendly_kind_with_article()
2803                    )));
2804                };
2805                let mut line_ctor = line_ctor.clone();
2806                // Which end of the line is this point?
2807                if line.start == point_id {
2808                    line_ctor.start = ctor.position;
2809                } else if line.end == point_id {
2810                    line_ctor.end = ctor.position;
2811                } else {
2812                    return Err(KclError::refactor(format!(
2813                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2814                    )));
2815                }
2816                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2817            }
2818
2819            // Handle Arc owner
2820            if let Segment::Arc(arc) = segment {
2821                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2822                    return Err(KclError::refactor(format!(
2823                        "Internal: Owner of point does not have arc ctor, but found {}",
2824                        arc.ctor.human_friendly_kind_with_article()
2825                    )));
2826                };
2827                let mut arc_ctor = arc_ctor.clone();
2828                // Which point of the arc is this? (center, start, or end)
2829                if arc.center == point_id {
2830                    arc_ctor.center = ctor.position;
2831                } else if arc.start == point_id {
2832                    arc_ctor.start = ctor.position;
2833                } else if arc.end == point_id {
2834                    arc_ctor.end = ctor.position;
2835                } else {
2836                    return Err(KclError::refactor(format!(
2837                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2838                    )));
2839                }
2840                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2841            }
2842
2843            // Handle Circle owner
2844            if let Segment::Circle(circle) = segment {
2845                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2846                    return Err(KclError::refactor(format!(
2847                        "Internal: Owner of point does not have circle ctor, but found {}",
2848                        circle.ctor.human_friendly_kind_with_article()
2849                    )));
2850                };
2851                let mut circle_ctor = circle_ctor.clone();
2852                if circle.center == point_id {
2853                    circle_ctor.center = ctor.position;
2854                } else if circle.start == point_id {
2855                    circle_ctor.start = ctor.position;
2856                } else {
2857                    return Err(KclError::refactor(format!(
2858                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2859                    )));
2860                }
2861                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2862            }
2863
2864            if let Segment::ControlPointSpline(spline) = segment {
2865                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2866                    return Err(KclError::refactor(format!(
2867                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2868                        spline.ctor.human_friendly_kind_with_article()
2869                    )));
2870                };
2871                let mut spline_ctor = spline_ctor.clone();
2872                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2873                    return Err(KclError::refactor(format!(
2874                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2875                    )));
2876                };
2877                spline_ctor.points[control_index] = ctor.position;
2878                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2879            }
2880
2881            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2882            // (fall through to the point editing logic below)
2883        }
2884
2885        // Modify the point AST.
2886        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2887        Ok(())
2888    }
2889
2890    fn edit_line(
2891        &mut self,
2892        new_ast: &mut ast::Node<ast::Program>,
2893        sketch: ObjectId,
2894        line: ObjectId,
2895        ctor: LineCtor,
2896    ) -> Result<(), KclError> {
2897        // Create updated KCL source from args.
2898        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2899        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2900
2901        // Look up existing sketch.
2902        let sketch_id = sketch;
2903        let sketch_object = self
2904            .scene_graph
2905            .objects
2906            .get(sketch_id.0)
2907            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2908        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2909            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2910        };
2911        sketch
2912            .segments
2913            .iter()
2914            .find(|o| **o == line)
2915            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2916        // Look up existing line.
2917        let line_id = line;
2918        let line_object = self
2919            .scene_graph
2920            .objects
2921            .get(line_id.0)
2922            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2923        let ObjectKind::Segment { .. } = &line_object.kind else {
2924            let kind = line_object.kind.human_friendly_kind_with_article();
2925            return Err(KclError::refactor(format!(
2926                "This constraint only works on Segments, but you selected {kind}"
2927            )));
2928        };
2929
2930        // Modify the line AST.
2931        self.mutate_ast(
2932            new_ast,
2933            line_id,
2934            AstMutateCommand::EditLine {
2935                start: new_start_ast,
2936                end: new_end_ast,
2937                construction: ctor.construction,
2938            },
2939        )?;
2940        Ok(())
2941    }
2942
2943    fn edit_arc(
2944        &mut self,
2945        new_ast: &mut ast::Node<ast::Program>,
2946        sketch: ObjectId,
2947        arc: ObjectId,
2948        ctor: ArcCtor,
2949    ) -> Result<(), KclError> {
2950        // Create updated KCL source from args.
2951        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2952        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2953        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2954
2955        // Look up existing sketch.
2956        let sketch_id = sketch;
2957        let sketch_object = self
2958            .scene_graph
2959            .objects
2960            .get(sketch_id.0)
2961            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2962        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2963            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2964        };
2965        sketch
2966            .segments
2967            .iter()
2968            .find(|o| **o == arc)
2969            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2970        // Look up existing arc.
2971        let arc_id = arc;
2972        let arc_object = self
2973            .scene_graph
2974            .objects
2975            .get(arc_id.0)
2976            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2977        let ObjectKind::Segment { .. } = &arc_object.kind else {
2978            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2979        };
2980
2981        // Modify the arc AST.
2982        self.mutate_ast(
2983            new_ast,
2984            arc_id,
2985            AstMutateCommand::EditArc {
2986                start: new_start_ast,
2987                end: new_end_ast,
2988                center: new_center_ast,
2989                direction: ctor.direction,
2990                construction: ctor.construction,
2991            },
2992        )?;
2993        Ok(())
2994    }
2995
2996    fn edit_circle(
2997        &mut self,
2998        new_ast: &mut ast::Node<ast::Program>,
2999        sketch: ObjectId,
3000        circle: ObjectId,
3001        ctor: CircleCtor,
3002    ) -> Result<(), KclError> {
3003        // Create updated KCL source from args.
3004        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3005        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3006
3007        // Look up existing sketch.
3008        let sketch_id = sketch;
3009        let sketch_object = self
3010            .scene_graph
3011            .objects
3012            .get(sketch_id.0)
3013            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3014        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3015            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3016        };
3017        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3018            KclError::refactor(format!(
3019                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3020            ))
3021        })?;
3022        // Look up existing circle.
3023        let circle_id = circle;
3024        let circle_object = self
3025            .scene_graph
3026            .objects
3027            .get(circle_id.0)
3028            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3029        let ObjectKind::Segment { .. } = &circle_object.kind else {
3030            return Err(KclError::refactor(format!(
3031                "Object is not a segment: {circle_object:?}"
3032            )));
3033        };
3034
3035        // Modify the circle AST.
3036        self.mutate_ast(
3037            new_ast,
3038            circle_id,
3039            AstMutateCommand::EditCircle {
3040                start: new_start_ast,
3041                center: new_center_ast,
3042                construction: ctor.construction,
3043            },
3044        )?;
3045        Ok(())
3046    }
3047
3048    fn edit_control_point_spline(
3049        &mut self,
3050        new_ast: &mut ast::Node<ast::Program>,
3051        sketch: ObjectId,
3052        spline: ObjectId,
3053        ctor: ControlPointSplineCtor,
3054    ) -> Result<(), KclError> {
3055        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3056
3057        let sketch_object = self
3058            .scene_graph
3059            .objects
3060            .get(sketch.0)
3061            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3062        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3063            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3064        };
3065        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3066            KclError::refactor(format!(
3067                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3068            ))
3069        })?;
3070
3071        let spline_object =
3072            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3073                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3074            })?;
3075        let ObjectKind::Segment { .. } = &spline_object.kind else {
3076            return Err(KclError::refactor(format!(
3077                "Object is not a segment: {spline_object:?}"
3078            )));
3079        };
3080
3081        self.mutate_ast(
3082            new_ast,
3083            spline,
3084            AstMutateCommand::EditControlPointSpline {
3085                points: points_ast,
3086                construction: ctor.construction,
3087            },
3088        )?;
3089        Ok(())
3090    }
3091
3092    fn delete_segment(
3093        &mut self,
3094        new_ast: &mut ast::Node<ast::Program>,
3095        sketch: ObjectId,
3096        segment_id: ObjectId,
3097    ) -> Result<(), KclError> {
3098        // Look up existing sketch.
3099        let sketch_id = sketch;
3100        let sketch_object = self
3101            .scene_graph
3102            .objects
3103            .get(sketch_id.0)
3104            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3105        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3106            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3107        };
3108        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3109            KclError::refactor(format!(
3110                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3111            ))
3112        })?;
3113        // Look up existing segment.
3114        let segment_object =
3115            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3116                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3117            })?;
3118        let ObjectKind::Segment { .. } = &segment_object.kind else {
3119            return Err(KclError::refactor(format!(
3120                "Object is not a segment, it is {}",
3121                segment_object.kind.human_friendly_kind_with_article()
3122            )));
3123        };
3124
3125        // Modify the AST to remove the segment.
3126        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3127        Ok(())
3128    }
3129
3130    fn delete_constraint(
3131        &mut self,
3132        new_ast: &mut ast::Node<ast::Program>,
3133        sketch: ObjectId,
3134        constraint_id: ObjectId,
3135    ) -> Result<(), KclError> {
3136        // Look up existing sketch.
3137        let sketch_id = sketch;
3138        let sketch_object = self
3139            .scene_graph
3140            .objects
3141            .get(sketch_id.0)
3142            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3143        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3144            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3145        };
3146        sketch
3147            .constraints
3148            .iter()
3149            .find(|o| **o == constraint_id)
3150            .ok_or_else(|| {
3151                KclError::refactor(format!(
3152                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3153                ))
3154            })?;
3155        // Look up existing constraint.
3156        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3157            KclError::refactor(format!(
3158                "Constraint not found in scene graph: constraint={constraint_id:?}"
3159            ))
3160        })?;
3161        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3162            return Err(KclError::refactor(format!(
3163                "Object is not a constraint, it is {}",
3164                constraint_object.kind.human_friendly_kind_with_article()
3165            )));
3166        };
3167
3168        // Modify the AST to remove the constraint.
3169        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3170        Ok(())
3171    }
3172
3173    fn edit_coincident_constraint(
3174        &mut self,
3175        new_ast: &mut ast::Node<ast::Program>,
3176        constraint_id: ObjectId,
3177        segments: Vec<ConstraintSegment>,
3178    ) -> Result<(), KclError> {
3179        if segments.len() < 2 {
3180            return Err(KclError::refactor(format!(
3181                "Coincident constraint must have at least 2 inputs, got {}",
3182                segments.len()
3183            )));
3184        }
3185
3186        let segment_asts = segments
3187            .iter()
3188            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3189            .collect::<Result<Vec<_>, _>>()?;
3190
3191        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3192            elements: segment_asts,
3193            digest: None,
3194            non_code_meta: Default::default(),
3195        })));
3196
3197        self.mutate_ast(
3198            new_ast,
3199            constraint_id,
3200            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3201        )?;
3202        Ok(())
3203    }
3204
3205    fn edit_horizontal_points_constraint(
3206        &mut self,
3207        new_ast: &mut ast::Node<ast::Program>,
3208        constraint_id: ObjectId,
3209        points: Vec<ConstraintSegment>,
3210    ) -> Result<(), KclError> {
3211        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3212    }
3213
3214    fn edit_vertical_points_constraint(
3215        &mut self,
3216        new_ast: &mut ast::Node<ast::Program>,
3217        constraint_id: ObjectId,
3218        points: Vec<ConstraintSegment>,
3219    ) -> Result<(), KclError> {
3220        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3221    }
3222
3223    fn edit_axis_points_constraint(
3224        &mut self,
3225        new_ast: &mut ast::Node<ast::Program>,
3226        constraint_id: ObjectId,
3227        points: Vec<ConstraintSegment>,
3228        constraint_name: &str,
3229    ) -> Result<(), KclError> {
3230        if points.len() < 2 {
3231            return Err(KclError::refactor(format!(
3232                "{constraint_name} points constraint must have at least 2 points, got {}",
3233                points.len()
3234            )));
3235        }
3236
3237        let point_asts = points
3238            .iter()
3239            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3240            .collect::<Result<Vec<_>, _>>()?;
3241
3242        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3243            elements: point_asts,
3244            digest: None,
3245            non_code_meta: Default::default(),
3246        })));
3247
3248        self.mutate_ast(
3249            new_ast,
3250            constraint_id,
3251            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3252        )?;
3253        Ok(())
3254    }
3255
3256    /// updates the equalLength constraint with the given lines
3257    fn edit_equal_length_constraint(
3258        &mut self,
3259        new_ast: &mut ast::Node<ast::Program>,
3260        constraint_id: ObjectId,
3261        lines: Vec<ObjectId>,
3262    ) -> Result<(), KclError> {
3263        if lines.len() < 2 {
3264            return Err(KclError::refactor(format!(
3265                "Lines equal length constraint must have at least 2 lines, got {}",
3266                lines.len()
3267            )));
3268        }
3269
3270        let line_asts = lines
3271            .iter()
3272            .map(|line_id| {
3273                let line_object = self
3274                    .scene_graph
3275                    .objects
3276                    .get(line_id.0)
3277                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3278                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3279                    let kind = line_object.kind.human_friendly_kind_with_article();
3280                    return Err(KclError::refactor(format!(
3281                        "This constraint only works on Segments, but you selected {kind}"
3282                    )));
3283                };
3284                let Segment::Line(_) = line_segment else {
3285                    let kind = line_segment.human_friendly_kind_with_article();
3286                    return Err(KclError::refactor(format!(
3287                        "Only lines can be made equal length, but you selected {kind}"
3288                    )));
3289                };
3290
3291                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3292            })
3293            .collect::<Result<Vec<_>, _>>()?;
3294
3295        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3296            elements: line_asts,
3297            digest: None,
3298            non_code_meta: Default::default(),
3299        })));
3300
3301        self.mutate_ast(
3302            new_ast,
3303            constraint_id,
3304            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3305        )?;
3306        Ok(())
3307    }
3308
3309    /// Updates the parallel constraint with the given lines.
3310    fn edit_parallel_constraint(
3311        &mut self,
3312        new_ast: &mut ast::Node<ast::Program>,
3313        constraint_id: ObjectId,
3314        lines: Vec<ObjectId>,
3315    ) -> Result<(), KclError> {
3316        if lines.len() < 2 {
3317            return Err(KclError::refactor(format!(
3318                "Parallel constraint must have at least 2 lines, got {}",
3319                lines.len()
3320            )));
3321        }
3322
3323        let line_asts = lines
3324            .iter()
3325            .map(|line_id| {
3326                let line_object = self
3327                    .scene_graph
3328                    .objects
3329                    .get(line_id.0)
3330                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3331                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3332                    let kind = line_object.kind.human_friendly_kind_with_article();
3333                    return Err(KclError::refactor(format!(
3334                        "This constraint only works on Segments, but you selected {kind}"
3335                    )));
3336                };
3337                let Segment::Line(_) = line_segment else {
3338                    let kind = line_segment.human_friendly_kind_with_article();
3339                    return Err(KclError::refactor(format!(
3340                        "Only lines can be made parallel, but you selected {kind}"
3341                    )));
3342                };
3343
3344                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3345            })
3346            .collect::<Result<Vec<_>, _>>()?;
3347
3348        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3349            elements: line_asts,
3350            digest: None,
3351            non_code_meta: Default::default(),
3352        })));
3353
3354        self.mutate_ast(
3355            new_ast,
3356            constraint_id,
3357            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3358        )?;
3359        Ok(())
3360    }
3361
3362    /// Updates the equalRadius constraint with the given segments.
3363    fn edit_equal_radius_constraint(
3364        &mut self,
3365        new_ast: &mut ast::Node<ast::Program>,
3366        constraint_id: ObjectId,
3367        input: Vec<ObjectId>,
3368    ) -> Result<(), KclError> {
3369        if input.len() < 2 {
3370            return Err(KclError::refactor(format!(
3371                "equalRadius constraint must have at least 2 segments, got {}",
3372                input.len()
3373            )));
3374        }
3375
3376        let input_asts = input
3377            .iter()
3378            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3379            .collect::<Result<Vec<_>, _>>()?;
3380
3381        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3382            elements: input_asts,
3383            digest: None,
3384            non_code_meta: Default::default(),
3385        })));
3386
3387        self.mutate_ast(
3388            new_ast,
3389            constraint_id,
3390            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3391        )?;
3392        Ok(())
3393    }
3394
3395    async fn execute_after_edit(
3396        &mut self,
3397        ctx: &ExecutorContext,
3398        sketch: ObjectId,
3399        sketch_block_ref: AstNodeRef,
3400        new_ast: &mut ast::Node<ast::Program>,
3401        options: ExecuteAfterEditOptions,
3402    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3403        let ExecuteAfterEditOptions {
3404            segment_ids_edited,
3405            edit_kind,
3406            commit_solved_initial_guesses,
3407        } = options;
3408
3409        // Convert to string source to create real source ranges.
3410        let new_source = source_from_ast(new_ast);
3411        // Parse the new KCL source.
3412        let new_program = parse_frontend_mutation_source(
3413            &new_source,
3414            "Error parsing KCL source after editing",
3415            "No AST produced after editing",
3416        )?;
3417
3418        // Truncate after the sketch block for mock execution.
3419        let is_delete = edit_kind.is_delete();
3420        let truncated_program = {
3421            let mut truncated_program = new_program.clone();
3422            only_sketch_block(
3423                &mut truncated_program.ast,
3424                &sketch_block_ref,
3425                edit_kind.to_change_kind(),
3426            )
3427            .map_err(KclErrorWithOutputs::no_outputs)?;
3428            truncated_program
3429        };
3430
3431        // Execute.
3432        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3433        let mock_config = MockConfig {
3434            sketch_block_id: Some(sketch),
3435            freedom_analysis: is_delete,
3436            segment_ids_edited: segment_ids_edited.clone(),
3437            drag_anchors,
3438            ..Default::default()
3439        };
3440        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3441
3442        // Only now, after execution has succeeded, update self.program.
3443        self.program = new_program;
3444
3445        // Uses freedom_analysis: is_delete
3446        let outcome = self.update_state_after_exec(outcome, is_delete);
3447
3448        let src_delta = if commit_solved_initial_guesses {
3449            self.commit_var_solutions_to_program(&outcome, "editing")?
3450        } else {
3451            SourceDelta { text: new_source }
3452        };
3453        let scene_graph_delta = SceneGraphDelta {
3454            new_graph: self.scene_graph_for_ui(),
3455            invalidates_ids: is_delete,
3456            new_objects: Vec::new(),
3457            exec_outcome: outcome,
3458        };
3459        Ok((src_delta, scene_graph_delta))
3460    }
3461
3462    async fn execute_after_delete_sketch(
3463        &mut self,
3464        ctx: &ExecutorContext,
3465        new_ast: &mut ast::Node<ast::Program>,
3466    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3467        // Convert to string source to create real source ranges.
3468        let new_source = source_from_ast(new_ast);
3469        // Parse the new KCL source.
3470        let new_program = parse_frontend_mutation_source(
3471            &new_source,
3472            "Error parsing KCL source after editing",
3473            "No AST produced after editing",
3474        )?;
3475
3476        // Make sure to only set this if there are no errors.
3477        self.program = new_program.clone();
3478
3479        // We deleted the entire sketch block. It doesn't make sense to truncate
3480        // and execute only the sketch block. We execute the whole program with
3481        // a real engine.
3482
3483        // Execute.
3484        let outcome = ctx.run_with_caching(new_program).await?;
3485        let freedom_analysis_ran = true;
3486
3487        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3488
3489        let src_delta = SourceDelta { text: new_source };
3490        let scene_graph_delta = SceneGraphDelta {
3491            new_graph: self.scene_graph_for_ui(),
3492            invalidates_ids: true,
3493            new_objects: Vec::new(),
3494            exec_outcome: outcome,
3495        };
3496        Ok((src_delta, scene_graph_delta))
3497    }
3498
3499    /// Map a point object id into an AST reference expression for use in
3500    /// constraints. If the point is owned by a segment (line or arc), we
3501    /// reference the appropriate property on that segment (e.g. `line1.start`,
3502    /// `arc1.center`). Otherwise we reference the point directly.
3503    fn point_id_to_ast_reference(
3504        &self,
3505        point_id: ObjectId,
3506        new_ast: &mut ast::Node<ast::Program>,
3507    ) -> Result<ast::Expr, KclError> {
3508        let point_object = self
3509            .scene_graph
3510            .objects
3511            .get(point_id.0)
3512            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3513        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3514            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3515        };
3516        let Segment::Point(point) = point_segment else {
3517            return Err(KclError::refactor(format!(
3518                "Only points are currently supported: {point_object:?}"
3519            )));
3520        };
3521
3522        if let Some(owner_id) = point.owner {
3523            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3524                KclError::refactor(format!(
3525                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3526                ))
3527            })?;
3528            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3529                return Err(KclError::refactor(format!(
3530                    "Owner of point is not a segment, but found {}",
3531                    owner_object.kind.human_friendly_kind_with_article()
3532                )));
3533            };
3534
3535            match owner_segment {
3536                Segment::Line(line) => {
3537                    let property = if line.start == point_id {
3538                        LINE_PROPERTY_START
3539                    } else if line.end == point_id {
3540                        LINE_PROPERTY_END
3541                    } else {
3542                        return Err(KclError::refactor(format!(
3543                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3544                        )));
3545                    };
3546                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3547                }
3548                Segment::Arc(arc) => {
3549                    let property = if arc.start == point_id {
3550                        ARC_PROPERTY_START
3551                    } else if arc.end == point_id {
3552                        ARC_PROPERTY_END
3553                    } else if arc.center == point_id {
3554                        ARC_PROPERTY_CENTER
3555                    } else {
3556                        return Err(KclError::refactor(format!(
3557                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3558                        )));
3559                    };
3560                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3561                }
3562                Segment::Circle(circle) => {
3563                    let property = if circle.start == point_id {
3564                        CIRCLE_PROPERTY_START
3565                    } else if circle.center == point_id {
3566                        CIRCLE_PROPERTY_CENTER
3567                    } else {
3568                        return Err(KclError::refactor(format!(
3569                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3570                        )));
3571                    };
3572                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3573                }
3574                Segment::ControlPointSpline(spline) => {
3575                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3576                        return Err(KclError::refactor(format!(
3577                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3578                        )));
3579                    };
3580                    let owner_expr =
3581                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3582                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3583                    Ok(create_index_expression(controls_expr, index))
3584                }
3585                _ => Err(KclError::refactor(format!(
3586                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3587                ))),
3588            }
3589        } else {
3590            // Standalone point.
3591            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3592        }
3593    }
3594
3595    fn line_id_to_ast_reference(
3596        &self,
3597        line_id: ObjectId,
3598        new_ast: &mut ast::Node<ast::Program>,
3599    ) -> Result<ast::Expr, KclError> {
3600        let line_object = self
3601            .scene_graph
3602            .objects
3603            .get(line_id.0)
3604            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3605        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3606            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3607        };
3608        let Segment::Line(line) = line_segment else {
3609            return Err(KclError::refactor(format!(
3610                "Only lines are currently supported: {line_object:?}"
3611            )));
3612        };
3613
3614        if let Some(owner_id) = line.owner {
3615            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3616                KclError::refactor(format!(
3617                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3618                ))
3619            })?;
3620            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3621                return Err(KclError::refactor(format!(
3622                    "Owner of line is not a segment, but found {}",
3623                    owner_object.kind.human_friendly_kind_with_article()
3624                )));
3625            };
3626
3627            match owner_segment {
3628                Segment::ControlPointSpline(spline) => {
3629                    let Some(index) = spline
3630                        .controls
3631                        .windows(2)
3632                        .position(|window| window[0] == line.start && window[1] == line.end)
3633                    else {
3634                        return Err(KclError::refactor(format!(
3635                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3636                        )));
3637                    };
3638                    let owner_expr =
3639                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3640                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3641                    Ok(create_index_expression(edges_expr, index))
3642                }
3643                _ => Err(KclError::refactor(format!(
3644                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3645                ))),
3646            }
3647        } else {
3648            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3649        }
3650    }
3651
3652    fn coincident_segment_to_ast(
3653        &self,
3654        segment: &ConstraintSegment,
3655        new_ast: &mut ast::Node<ast::Program>,
3656    ) -> Result<ast::Expr, KclError> {
3657        match segment {
3658            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3659            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3660        }
3661    }
3662
3663    fn segment_id_to_constraint_ast_reference(
3664        &self,
3665        segment_id: ObjectId,
3666        new_ast: &mut ast::Node<ast::Program>,
3667    ) -> Result<ast::Expr, KclError> {
3668        let segment_object = self
3669            .scene_graph
3670            .objects
3671            .get(segment_id.0)
3672            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3673        let ObjectKind::Segment { segment } = &segment_object.kind else {
3674            return Err(KclError::refactor(format!(
3675                "Object is not a segment, it is {}",
3676                segment_object.kind.human_friendly_kind_with_article()
3677            )));
3678        };
3679
3680        match segment {
3681            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3682            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3683            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3684            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3685            Segment::ControlPointSpline(_) => {
3686                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3687            }
3688        }
3689    }
3690
3691    fn axis_constraint_segment_to_ast(
3692        &self,
3693        segment: &ConstraintSegment,
3694        new_ast: &mut ast::Node<ast::Program>,
3695    ) -> Result<ast::Expr, KclError> {
3696        match segment {
3697            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3698            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3699        }
3700    }
3701
3702    async fn add_coincident(
3703        &mut self,
3704        sketch: ObjectId,
3705        coincident: Coincident,
3706        new_ast: &mut ast::Node<ast::Program>,
3707    ) -> Result<AstNodeRef, KclError> {
3708        let sketch_id = sketch;
3709        for segment in &coincident.segments {
3710            let ConstraintSegment::Segment(segment_id) = segment else {
3711                continue;
3712            };
3713            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3714                continue;
3715            };
3716            if matches!(
3717                segment_object.kind,
3718                ObjectKind::Segment {
3719                    segment: Segment::ControlPointSpline(_)
3720                }
3721            ) {
3722                return Err(KclError::refactor(
3723                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3724                        .to_owned(),
3725                ));
3726            }
3727        }
3728        let segment_asts = coincident
3729            .segments
3730            .iter()
3731            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3732            .collect::<Result<Vec<_>, _>>()?;
3733        if segment_asts.len() < 2 {
3734            return Err(KclError::refactor(format!(
3735                "Coincident constraint must have at least 2 inputs, got {}",
3736                segment_asts.len()
3737            )));
3738        }
3739
3740        // Create the coincident() call using shared helper.
3741        let coincident_ast = create_coincident_ast(segment_asts);
3742
3743        // Add the line to the AST of the sketch block.
3744        let (sketch_block_ref, _) = self.mutate_ast(
3745            new_ast,
3746            sketch_id,
3747            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3748        )?;
3749        Ok(sketch_block_ref)
3750    }
3751
3752    async fn add_distance(
3753        &mut self,
3754        sketch: ObjectId,
3755        distance: Distance,
3756        new_ast: &mut ast::Node<ast::Program>,
3757    ) -> Result<AstNodeRef, KclError> {
3758        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3759    }
3760
3761    fn distance_constraint_ast_parts(
3762        &self,
3763        function_name: &str,
3764        distance: &Distance,
3765        new_ast: &mut ast::Node<ast::Program>,
3766    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3767        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3768            [pt0, pt1] => [
3769                self.coincident_segment_to_ast(pt0, new_ast)?,
3770                self.coincident_segment_to_ast(pt1, new_ast)?,
3771            ],
3772            _ => {
3773                return Err(KclError::refactor(format!(
3774                    "Distance constraint must have exactly 2 segments, got {}",
3775                    distance.segments.len()
3776                )));
3777            }
3778        };
3779
3780        let arguments = match &distance.label_position {
3781            Some(label_position) => vec![ast::LabeledArg {
3782                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3783                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3784            }],
3785            None => Default::default(),
3786        };
3787
3788        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3789            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3790            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3791                ast::ArrayExpression {
3792                    elements: vec![segment0_ast, segment1_ast],
3793                    digest: None,
3794                    non_code_meta: Default::default(),
3795                },
3796            )))),
3797            arguments,
3798            digest: None,
3799            non_code_meta: Default::default(),
3800        })));
3801        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3802            value: ast::LiteralValue::Number {
3803                value: distance.distance.value,
3804                suffix: distance.distance.units,
3805            },
3806            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3807                KclError::refactor(format!(
3808                    "Could not format numeric suffix: {:?}",
3809                    distance.distance.units
3810                ))
3811            })?,
3812            digest: None,
3813        })));
3814
3815        Ok((call, value))
3816    }
3817
3818    fn add_distance_constraint(
3819        &mut self,
3820        sketch: ObjectId,
3821        function_name: &str,
3822        distance: Distance,
3823        new_ast: &mut ast::Node<ast::Program>,
3824    ) -> Result<AstNodeRef, KclError> {
3825        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3826        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3827            left: call,
3828            operator: ast::BinaryOperator::Eq,
3829            right: value,
3830            digest: None,
3831        })));
3832
3833        let (sketch_block_ref, _) = self.mutate_ast(
3834            new_ast,
3835            sketch,
3836            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3837        )?;
3838        Ok(sketch_block_ref)
3839    }
3840
3841    async fn add_angle(
3842        &mut self,
3843        sketch: ObjectId,
3844        angle: Angle,
3845        new_ast: &mut ast::Node<ast::Program>,
3846    ) -> Result<AstNodeRef, KclError> {
3847        let sketch_id = sketch;
3848        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3849        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3850            left: angle_call_ast,
3851            operator: ast::BinaryOperator::Eq,
3852            right: angle_value_ast,
3853            digest: None,
3854        })));
3855
3856        // Add the line to the AST of the sketch block.
3857        let (sketch_block_ref, _) = self.mutate_ast(
3858            new_ast,
3859            sketch_id,
3860            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3861        )?;
3862        Ok(sketch_block_ref)
3863    }
3864
3865    fn angle_constraint_ast_parts(
3866        &self,
3867        angle: &Angle,
3868        new_ast: &mut ast::Node<ast::Program>,
3869    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3870        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3871            return Err(KclError::refactor(format!(
3872                "Angle constraint must have exactly 2 lines, got {}",
3873                angle.lines.len()
3874            )));
3875        };
3876
3877        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3878        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3879        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3880            elements: vec![l0_ast, l1_ast],
3881            digest: None,
3882            non_code_meta: Default::default(),
3883        })));
3884
3885        if angle.inverse == Some(true) && angle.sector.is_none() {
3886            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3887        }
3888
3889        let uses_angle_dimension = angle.sector.is_some();
3890        let mut arguments = if uses_angle_dimension {
3891            vec![ast::LabeledArg {
3892                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3893                arg: lines_ast.clone(),
3894            }]
3895        } else {
3896            Default::default()
3897        };
3898
3899        if let Some(sector) = angle.sector {
3900            arguments.push(ast::LabeledArg {
3901                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3902                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3903                    value: ast::LiteralValue::Number {
3904                        value: f64::from(sector),
3905                        suffix: NumericSuffix::None,
3906                    },
3907                    raw: sector.to_string(),
3908                    digest: None,
3909                }))),
3910            });
3911        }
3912
3913        if angle.inverse == Some(true) {
3914            arguments.push(ast::LabeledArg {
3915                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3916                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3917                    value: ast::LiteralValue::Bool(true),
3918                    raw: true.to_string(),
3919                    digest: None,
3920                }))),
3921            });
3922        }
3923
3924        if let Some(label_position) = &angle.label_position {
3925            arguments.push(ast::LabeledArg {
3926                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3927                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3928            });
3929        }
3930
3931        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3932            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3933                ANGLE_DIMENSION_FN
3934            } else {
3935                ANGLE_FN
3936            })),
3937            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3938            arguments,
3939            digest: None,
3940            non_code_meta: Default::default(),
3941        })));
3942        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3943            value: ast::LiteralValue::Number {
3944                value: angle.angle.value,
3945                suffix: angle.angle.units,
3946            },
3947            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3948                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3949            digest: None,
3950        })));
3951
3952        Ok((call, value))
3953    }
3954
3955    async fn add_tangent(
3956        &mut self,
3957        sketch: ObjectId,
3958        tangent: Tangent,
3959        new_ast: &mut ast::Node<ast::Program>,
3960    ) -> Result<AstNodeRef, KclError> {
3961        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3962            return Err(KclError::refactor(format!(
3963                "Tangent constraint must have exactly 2 segments, got {}",
3964                tangent.input.len()
3965            )));
3966        };
3967        let sketch_id = sketch;
3968
3969        let seg0_object = self
3970            .scene_graph
3971            .objects
3972            .get(seg0_id.0)
3973            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3974        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3975            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3976        };
3977        let seg0_ast = match seg0_segment {
3978            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3979                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3980            }
3981            _ => {
3982                return Err(KclError::refactor(format!(
3983                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3984                )));
3985            }
3986        };
3987
3988        let seg1_object = self
3989            .scene_graph
3990            .objects
3991            .get(seg1_id.0)
3992            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3993        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3994            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3995        };
3996        let seg1_ast = match seg1_segment {
3997            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3998                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3999            }
4000            _ => {
4001                return Err(KclError::refactor(format!(
4002                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4003                )));
4004            }
4005        };
4006
4007        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4008        let (sketch_block_ref, _) = self.mutate_ast(
4009            new_ast,
4010            sketch_id,
4011            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4012        )?;
4013        Ok(sketch_block_ref)
4014    }
4015
4016    async fn add_symmetric(
4017        &mut self,
4018        sketch: ObjectId,
4019        symmetric: Symmetric,
4020        new_ast: &mut ast::Node<ast::Program>,
4021    ) -> Result<AstNodeRef, KclError> {
4022        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4023            return Err(KclError::refactor(format!(
4024                "Symmetric constraint must have exactly 2 inputs, got {}",
4025                symmetric.input.len()
4026            )));
4027        };
4028        let sketch_id = sketch;
4029
4030        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4031        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4032        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4033
4034        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4035        let (sketch_block_ref, _) = self.mutate_ast(
4036            new_ast,
4037            sketch_id,
4038            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4039        )?;
4040        Ok(sketch_block_ref)
4041    }
4042
4043    async fn add_midpoint(
4044        &mut self,
4045        sketch: ObjectId,
4046        midpoint: Midpoint,
4047        new_ast: &mut ast::Node<ast::Program>,
4048    ) -> Result<AstNodeRef, KclError> {
4049        let sketch_id = sketch;
4050        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4051
4052        let segment_object = self
4053            .scene_graph
4054            .objects
4055            .get(midpoint.segment.0)
4056            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4057        let ObjectKind::Segment {
4058            segment: midpoint_segment,
4059        } = &segment_object.kind
4060        else {
4061            return Err(KclError::refactor(format!(
4062                "Object must be a segment, but it was {}",
4063                segment_object.kind.human_friendly_kind_with_article()
4064            )));
4065        };
4066        let segment_ast = match midpoint_segment {
4067            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4068            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4069            _ => {
4070                return Err(KclError::refactor(format!(
4071                    "Midpoint target must be a line or arc segment but it was {}",
4072                    midpoint_segment.human_friendly_kind_with_article()
4073                )));
4074            }
4075        };
4076
4077        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4078        let (sketch_block_ref, _) = self.mutate_ast(
4079            new_ast,
4080            sketch_id,
4081            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4082        )?;
4083        Ok(sketch_block_ref)
4084    }
4085
4086    async fn add_equal_radius(
4087        &mut self,
4088        sketch: ObjectId,
4089        equal_radius: EqualRadius,
4090        new_ast: &mut ast::Node<ast::Program>,
4091    ) -> Result<AstNodeRef, KclError> {
4092        if equal_radius.input.len() < 2 {
4093            return Err(KclError::refactor(format!(
4094                "equalRadius constraint must have at least 2 segments, got {}",
4095                equal_radius.input.len()
4096            )));
4097        }
4098
4099        let sketch_id = sketch;
4100        let input_asts = equal_radius
4101            .input
4102            .iter()
4103            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4104            .collect::<Result<Vec<_>, _>>()?;
4105
4106        let equal_radius_ast = create_equal_radius_ast(input_asts);
4107        let (sketch_block_ref, _) = self.mutate_ast(
4108            new_ast,
4109            sketch_id,
4110            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4111        )?;
4112        Ok(sketch_block_ref)
4113    }
4114
4115    async fn add_radius(
4116        &mut self,
4117        sketch: ObjectId,
4118        radius: Radius,
4119        new_ast: &mut ast::Node<ast::Program>,
4120    ) -> Result<AstNodeRef, KclError> {
4121        let params = ArcSizeConstraintParams {
4122            points: vec![radius.arc],
4123            function_name: RADIUS_FN,
4124            value: radius.radius.value,
4125            units: radius.radius.units,
4126            label_position: radius.label_position,
4127            constraint_type_name: "Radius",
4128        };
4129        self.add_arc_size_constraint(sketch, params, new_ast).await
4130    }
4131
4132    async fn add_diameter(
4133        &mut self,
4134        sketch: ObjectId,
4135        diameter: Diameter,
4136        new_ast: &mut ast::Node<ast::Program>,
4137    ) -> Result<AstNodeRef, KclError> {
4138        let params = ArcSizeConstraintParams {
4139            points: vec![diameter.arc],
4140            function_name: DIAMETER_FN,
4141            value: diameter.diameter.value,
4142            units: diameter.diameter.units,
4143            label_position: diameter.label_position,
4144            constraint_type_name: "Diameter",
4145        };
4146        self.add_arc_size_constraint(sketch, params, new_ast).await
4147    }
4148
4149    async fn add_fixed_constraints(
4150        &mut self,
4151        sketch: ObjectId,
4152        points: Vec<FixedPoint>,
4153        new_ast: &mut ast::Node<ast::Program>,
4154    ) -> Result<AstNodeRef, KclError> {
4155        let mut sketch_block_ref = None;
4156
4157        for fixed_point in points {
4158            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4159            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4160                .map_err(|err| KclError::refactor(err.to_string()))?;
4161
4162            let (sketch_ref, _) = self.mutate_ast(
4163                new_ast,
4164                sketch,
4165                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4166            )?;
4167            sketch_block_ref = Some(sketch_ref);
4168        }
4169
4170        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4171    }
4172
4173    async fn add_arc_size_constraint(
4174        &mut self,
4175        sketch: ObjectId,
4176        params: ArcSizeConstraintParams,
4177        new_ast: &mut ast::Node<ast::Program>,
4178    ) -> Result<AstNodeRef, KclError> {
4179        let sketch_id = sketch;
4180
4181        // Constraint must have exactly 1 argument (arc segment)
4182        if params.points.len() != 1 {
4183            return Err(KclError::refactor(format!(
4184                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4185                params.constraint_type_name,
4186                params.points.len()
4187            )));
4188        }
4189
4190        let arc_id = params.points[0];
4191        let arc_object = self
4192            .scene_graph
4193            .objects
4194            .get(arc_id.0)
4195            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4196        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4197            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4198        };
4199        let ref_type = match arc_segment {
4200            Segment::Arc(_) => ARC_VARIABLE,
4201            Segment::Circle(_) => CIRCLE_VARIABLE,
4202            _ => {
4203                return Err(KclError::refactor(format!(
4204                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4205                    params.constraint_type_name
4206                )));
4207            }
4208        };
4209        // Reference the arc/circle segment directly
4210        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4211        let arguments = match &params.label_position {
4212            Some(label_position) => vec![ast::LabeledArg {
4213                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4214                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4215            }],
4216            None => Default::default(),
4217        };
4218
4219        // Create the function call.
4220        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4221            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4222            unlabeled: Some(arc_ast),
4223            arguments,
4224            digest: None,
4225            non_code_meta: Default::default(),
4226        })));
4227        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4228            left: call_ast,
4229            operator: ast::BinaryOperator::Eq,
4230            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4231                value: ast::LiteralValue::Number {
4232                    value: params.value,
4233                    suffix: params.units,
4234                },
4235                raw: format_number_literal(params.value, params.units, None)
4236                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4237                digest: None,
4238            }))),
4239            digest: None,
4240        })));
4241
4242        // Add the line to the AST of the sketch block.
4243        let (sketch_block_ref, _) = self.mutate_ast(
4244            new_ast,
4245            sketch_id,
4246            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4247        )?;
4248        Ok(sketch_block_ref)
4249    }
4250
4251    async fn add_horizontal_distance(
4252        &mut self,
4253        sketch: ObjectId,
4254        distance: Distance,
4255        new_ast: &mut ast::Node<ast::Program>,
4256    ) -> Result<AstNodeRef, KclError> {
4257        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4258    }
4259
4260    async fn add_vertical_distance(
4261        &mut self,
4262        sketch: ObjectId,
4263        distance: Distance,
4264        new_ast: &mut ast::Node<ast::Program>,
4265    ) -> Result<AstNodeRef, KclError> {
4266        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4267    }
4268
4269    async fn add_horizontal(
4270        &mut self,
4271        sketch: ObjectId,
4272        horizontal: Horizontal,
4273        new_ast: &mut ast::Node<ast::Program>,
4274    ) -> Result<AstNodeRef, KclError> {
4275        let sketch_id = sketch;
4276
4277        // Map the runtime objects back to variable names.
4278        let first_arg_ast = match horizontal {
4279            Horizontal::Line { line } => {
4280                let line_object = self
4281                    .scene_graph
4282                    .objects
4283                    .get(line.0)
4284                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4285                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4286                    let kind = line_object.kind.human_friendly_kind_with_article();
4287                    return Err(KclError::refactor(format!(
4288                        "This constraint only works on Segments, but you selected {kind}"
4289                    )));
4290                };
4291                let Segment::Line(_) = line_segment else {
4292                    return Err(KclError::refactor(format!(
4293                        "Only lines can be made horizontal, but you selected {}",
4294                        line_segment.human_friendly_kind_with_article(),
4295                    )));
4296                };
4297                self.line_id_to_ast_reference(line, new_ast)?
4298            }
4299            Horizontal::Points { points } => {
4300                let point_asts = points
4301                    .iter()
4302                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4303                    .collect::<Result<Vec<_>, _>>()?;
4304                ast::ArrayExpression::new(point_asts).into()
4305            }
4306        };
4307        // Create the horizontal() call using shared helper.
4308        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4309
4310        // Add the line to the AST of the sketch block.
4311        let (sketch_block_ref, _) = self.mutate_ast(
4312            new_ast,
4313            sketch_id,
4314            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4315        )?;
4316        Ok(sketch_block_ref)
4317    }
4318
4319    async fn add_lines_equal_length(
4320        &mut self,
4321        sketch: ObjectId,
4322        lines_equal_length: LinesEqualLength,
4323        new_ast: &mut ast::Node<ast::Program>,
4324    ) -> Result<AstNodeRef, KclError> {
4325        if lines_equal_length.lines.len() < 2 {
4326            return Err(KclError::refactor(format!(
4327                "Lines equal length constraint must have at least 2 lines, got {}",
4328                lines_equal_length.lines.len()
4329            )));
4330        };
4331
4332        let sketch_id = sketch;
4333
4334        // Map the runtime objects back to variable names.
4335        let line_asts = lines_equal_length
4336            .lines
4337            .iter()
4338            .map(|line_id| {
4339                let line_object = self
4340                    .scene_graph
4341                    .objects
4342                    .get(line_id.0)
4343                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4344                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4345                    let kind = line_object.kind.human_friendly_kind_with_article();
4346                    return Err(KclError::refactor(format!(
4347                        "This constraint only works on Segments, but you selected {kind}"
4348                    )));
4349                };
4350                let Segment::Line(_) = line_segment else {
4351                    let kind = line_segment.human_friendly_kind_with_article();
4352                    return Err(KclError::refactor(format!(
4353                        "Only lines can be made equal length, but you selected {kind}"
4354                    )));
4355                };
4356
4357                self.line_id_to_ast_reference(*line_id, new_ast)
4358            })
4359            .collect::<Result<Vec<_>, _>>()?;
4360
4361        // Create the equalLength() call using shared helper.
4362        let equal_length_ast = create_equal_length_ast(line_asts);
4363
4364        // Add the constraint to the AST of the sketch block.
4365        let (sketch_block_ref, _) = self.mutate_ast(
4366            new_ast,
4367            sketch_id,
4368            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4369        )?;
4370        Ok(sketch_block_ref)
4371    }
4372
4373    fn equal_radius_segment_id_to_ast_reference(
4374        &mut self,
4375        segment_id: ObjectId,
4376        new_ast: &mut ast::Node<ast::Program>,
4377    ) -> Result<ast::Expr, KclError> {
4378        let segment_object = self
4379            .scene_graph
4380            .objects
4381            .get(segment_id.0)
4382            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4383        let ObjectKind::Segment { segment } = &segment_object.kind else {
4384            return Err(KclError::refactor(format!(
4385                "Object is not a segment, it was {}",
4386                segment_object.kind.human_friendly_kind_with_article()
4387            )));
4388        };
4389
4390        let ref_type = match segment {
4391            Segment::Arc(_) => ARC_VARIABLE,
4392            Segment::Circle(_) => CIRCLE_VARIABLE,
4393            _ => {
4394                return Err(KclError::refactor(format!(
4395                    "equalRadius supports only arc/circle segments, got {}",
4396                    segment.human_friendly_kind_with_article()
4397                )));
4398            }
4399        };
4400
4401        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4402    }
4403
4404    fn symmetric_input_id_to_ast_reference(
4405        &mut self,
4406        segment_id: ObjectId,
4407        new_ast: &mut ast::Node<ast::Program>,
4408    ) -> Result<ast::Expr, KclError> {
4409        let segment_object = self
4410            .scene_graph
4411            .objects
4412            .get(segment_id.0)
4413            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4414        let ObjectKind::Segment { segment } = &segment_object.kind else {
4415            return Err(KclError::refactor(format!(
4416                "Object is not a segment, it was {}",
4417                segment_object.kind.human_friendly_kind_with_article()
4418            )));
4419        };
4420
4421        match segment {
4422            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4423            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4424            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4425            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4426            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4427                "Symmetric does not yet support control point splines".to_owned(),
4428            )),
4429        }
4430    }
4431
4432    fn symmetric_axis_id_to_ast_reference(
4433        &mut self,
4434        segment_id: ObjectId,
4435        new_ast: &mut ast::Node<ast::Program>,
4436    ) -> Result<ast::Expr, KclError> {
4437        let segment_object = self
4438            .scene_graph
4439            .objects
4440            .get(segment_id.0)
4441            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4442        let ObjectKind::Segment { segment } = &segment_object.kind else {
4443            return Err(KclError::refactor(format!(
4444                "Object is not a segment, it was {}",
4445                segment_object.kind.human_friendly_kind_with_article()
4446            )));
4447        };
4448        match segment {
4449            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4450            _ => Err(KclError::refactor(format!(
4451                "Symmetric axis must be a line, got {}",
4452                segment.human_friendly_kind_with_article()
4453            ))),
4454        }
4455    }
4456
4457    async fn add_parallel(
4458        &mut self,
4459        sketch: ObjectId,
4460        parallel: Parallel,
4461        new_ast: &mut ast::Node<ast::Program>,
4462    ) -> Result<AstNodeRef, KclError> {
4463        if parallel.lines.len() < 2 {
4464            return Err(KclError::refactor(format!(
4465                "Parallel constraint must have at least 2 lines, got {}",
4466                parallel.lines.len()
4467            )));
4468        };
4469
4470        let sketch_id = sketch;
4471
4472        let line_asts = parallel
4473            .lines
4474            .iter()
4475            .map(|line_id| {
4476                let line_object = self
4477                    .scene_graph
4478                    .objects
4479                    .get(line_id.0)
4480                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4481                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4482                    let kind = line_object.kind.human_friendly_kind_with_article();
4483                    return Err(KclError::refactor(format!(
4484                        "This constraint only works on Segments, but you selected {kind}"
4485                    )));
4486                };
4487                let Segment::Line(_) = line_segment else {
4488                    let kind = line_segment.human_friendly_kind_with_article();
4489                    return Err(KclError::refactor(format!(
4490                        "Only lines can be made parallel, but you selected {kind}"
4491                    )));
4492                };
4493
4494                self.line_id_to_ast_reference(*line_id, new_ast)
4495            })
4496            .collect::<Result<Vec<_>, _>>()?;
4497
4498        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4499            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4500            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4501                ast::ArrayExpression {
4502                    elements: line_asts,
4503                    digest: None,
4504                    non_code_meta: Default::default(),
4505                },
4506            )))),
4507            arguments: Default::default(),
4508            digest: None,
4509            non_code_meta: Default::default(),
4510        })));
4511
4512        let (sketch_block_ref, _) = self.mutate_ast(
4513            new_ast,
4514            sketch_id,
4515            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4516        )?;
4517        Ok(sketch_block_ref)
4518    }
4519
4520    async fn add_perpendicular(
4521        &mut self,
4522        sketch: ObjectId,
4523        perpendicular: Perpendicular,
4524        new_ast: &mut ast::Node<ast::Program>,
4525    ) -> Result<AstNodeRef, KclError> {
4526        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4527            .await
4528    }
4529
4530    async fn add_lines_at_angle_constraint(
4531        &mut self,
4532        sketch: ObjectId,
4533        angle_kind: LinesAtAngleKind,
4534        lines: Vec<ObjectId>,
4535        new_ast: &mut ast::Node<ast::Program>,
4536    ) -> Result<AstNodeRef, KclError> {
4537        let &[line0_id, line1_id] = lines.as_slice() else {
4538            return Err(KclError::refactor(format!(
4539                "{} constraint must have exactly 2 lines, got {}",
4540                angle_kind.to_function_name(),
4541                lines.len()
4542            )));
4543        };
4544
4545        let sketch_id = sketch;
4546
4547        // Map the runtime objects back to variable names.
4548        let line0_object = self
4549            .scene_graph
4550            .objects
4551            .get(line0_id.0)
4552            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4553        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4554            let kind = line0_object.kind.human_friendly_kind_with_article();
4555            return Err(KclError::refactor(format!(
4556                "This constraint only works on Segments, but you selected {kind}"
4557            )));
4558        };
4559        let Segment::Line(_) = line0_segment else {
4560            return Err(KclError::refactor(format!(
4561                "Only lines can be made {}, but you selected {}",
4562                angle_kind.to_function_name(),
4563                line0_segment.human_friendly_kind_with_article(),
4564            )));
4565        };
4566        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4567
4568        let line1_object = self
4569            .scene_graph
4570            .objects
4571            .get(line1_id.0)
4572            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4573        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4574            let kind = line1_object.kind.human_friendly_kind_with_article();
4575            return Err(KclError::refactor(format!(
4576                "This constraint only works on Segments, but you selected {kind}"
4577            )));
4578        };
4579        let Segment::Line(_) = line1_segment else {
4580            return Err(KclError::refactor(format!(
4581                "Only lines can be made {}, but you selected {}",
4582                angle_kind.to_function_name(),
4583                line1_segment.human_friendly_kind_with_article(),
4584            )));
4585        };
4586        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4587
4588        // Create the parallel() or perpendicular() call.
4589        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4590            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4591            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4592                ast::ArrayExpression {
4593                    elements: vec![line0_ast, line1_ast],
4594                    digest: None,
4595                    non_code_meta: Default::default(),
4596                },
4597            )))),
4598            arguments: Default::default(),
4599            digest: None,
4600            non_code_meta: Default::default(),
4601        })));
4602
4603        // Add the constraint to the AST of the sketch block.
4604        let (sketch_block_ref, _) = self.mutate_ast(
4605            new_ast,
4606            sketch_id,
4607            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4608        )?;
4609        Ok(sketch_block_ref)
4610    }
4611
4612    async fn add_vertical(
4613        &mut self,
4614        sketch: ObjectId,
4615        vertical: Vertical,
4616        new_ast: &mut ast::Node<ast::Program>,
4617    ) -> Result<AstNodeRef, KclError> {
4618        let sketch_id = sketch;
4619
4620        let first_arg_ast = match vertical {
4621            Vertical::Line { line } => {
4622                // Map the runtime objects back to variable names.
4623                let line_object = self
4624                    .scene_graph
4625                    .objects
4626                    .get(line.0)
4627                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4628                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4629                    let kind = line_object.kind.human_friendly_kind_with_article();
4630                    return Err(KclError::refactor(format!(
4631                        "This constraint only works on Segments, but you selected {kind}"
4632                    )));
4633                };
4634                let Segment::Line(_) = line_segment else {
4635                    return Err(KclError::refactor(format!(
4636                        "Only lines can be made vertical, but you selected {}",
4637                        line_segment.human_friendly_kind_with_article()
4638                    )));
4639                };
4640                self.line_id_to_ast_reference(line, new_ast)?
4641            }
4642            Vertical::Points { points } => {
4643                let point_asts = points
4644                    .iter()
4645                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4646                    .collect::<Result<Vec<_>, _>>()?;
4647                ast::ArrayExpression::new(point_asts).into()
4648            }
4649        };
4650        // Create the vertical() call using shared helper.
4651        let vertical_ast = create_vertical_ast(first_arg_ast);
4652
4653        // Add the line to the AST of the sketch block.
4654        let (sketch_block_ref, _) = self.mutate_ast(
4655            new_ast,
4656            sketch_id,
4657            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4658        )?;
4659        Ok(sketch_block_ref)
4660    }
4661
4662    async fn execute_after_add_constraint(
4663        &mut self,
4664        ctx: &ExecutorContext,
4665        sketch_id: ObjectId,
4666        sketch_block_ref: AstNodeRef,
4667        new_ast: &mut ast::Node<ast::Program>,
4668    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4669        // Convert to string source to create real source ranges.
4670        let new_source = source_from_ast(new_ast);
4671        // Parse the new KCL source.
4672        let new_program = parse_frontend_mutation_source(
4673            &new_source,
4674            "Error parsing KCL source after adding constraint",
4675            "No AST produced after adding constraint",
4676        )?;
4677        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4678            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4679                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4680            )))
4681        })?;
4682
4683        // Truncate after the sketch block for mock execution.
4684        // Use a clone so we don't mutate new_program yet
4685        let mut truncated_program = new_program.clone();
4686        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4687            .map_err(KclErrorWithOutputs::no_outputs)?;
4688
4689        // Execute - if this fails, we haven't modified self yet, so state is safe
4690        let outcome = ctx
4691            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4692            .await?;
4693
4694        let new_object_ids = {
4695            // Extract the constraint ID from the execution outcome using source_range_to_object
4696            let constraint_id = outcome
4697                .source_range_to_object
4698                .get(&constraint_node_ref.range)
4699                .copied()
4700                .ok_or_else(|| {
4701                    KclErrorWithOutputs::from_error_outcome(
4702                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4703                        outcome.clone(),
4704                    )
4705                })?;
4706            vec![constraint_id]
4707        };
4708
4709        // Only now, after all operations succeeded, update self.program.
4710        // This ensures state is only modified if everything succeeds.
4711        self.program = new_program;
4712
4713        // Uses MockConfig::default() which has freedom_analysis: true
4714        let outcome = self.update_state_after_exec(outcome, true);
4715
4716        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4717        let scene_graph_delta = SceneGraphDelta {
4718            new_graph: self.scene_graph_for_ui(),
4719            invalidates_ids: false,
4720            new_objects: new_object_ids,
4721            exec_outcome: outcome,
4722        };
4723        Ok((src_delta, scene_graph_delta))
4724    }
4725
4726    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4727        let commit_failure = || {
4728            KclErrorWithOutputs::from_error_outcome(
4729                KclError::refactor(format!("Could not update KCL after {operation}.")),
4730                outcome.clone(),
4731            )
4732        };
4733
4734        let default_length_unit = self.default_length_unit();
4735        let mut settled_ast = self.program.ast.clone();
4736        let mut committed_solver_value = false;
4737        for (var_range, node_path, value) in &outcome.var_solutions {
4738            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4739                return Err(commit_failure());
4740            };
4741            let new_value = match &lookup {
4742                Some(current_literal) => {
4743                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4744                        continue;
4745                    }
4746                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4747                }
4748                None => {
4749                    // Bare `var` with no initial literal to compare against;
4750                    // always commit, using the module's default length unit as
4751                    // an explicit suffix so the written value carries units.
4752                    Number {
4753                        value: number_value_in_default_length_units(*value, default_length_unit),
4754                        units: default_length_unit.into(),
4755                    }
4756                }
4757            };
4758            committed_solver_value = true;
4759            let source_ref = SourceRef::Simple {
4760                range: *var_range,
4761                node_path: node_path.clone(),
4762            };
4763            mutate_ast_node_by_source_ref(
4764                &mut settled_ast,
4765                &source_ref,
4766                AstMutateCommand::EditVarInitialValue { value: new_value },
4767            )
4768            .map_err(|_| commit_failure())?;
4769        }
4770
4771        if !committed_solver_value {
4772            return Ok(SourceDelta {
4773                text: self.program.original_file_contents.clone(),
4774            });
4775        }
4776
4777        let settled_source = source_from_ast(&settled_ast);
4778        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4779        if !errors.is_empty() {
4780            return Err(commit_failure());
4781        }
4782        let Some(settled_program) = settled_program else {
4783            return Err(commit_failure());
4784        };
4785
4786        self.program = settled_program;
4787
4788        Ok(SourceDelta { text: settled_source })
4789    }
4790
4791    // Find constraints that reference the given segments.
4792    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4793        if segment_ids_set.contains(&segment_id) {
4794            return true;
4795        }
4796
4797        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4798            return false;
4799        };
4800        let ObjectKind::Segment { segment } = &segment_object.kind else {
4801            return false;
4802        };
4803        let Segment::Point(point) = segment else {
4804            return false;
4805        };
4806
4807        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4808    }
4809
4810    fn remaining_constraint_segments(
4811        &self,
4812        segments: &[ConstraintSegment],
4813        segment_ids_set: &AhashIndexSet<ObjectId>,
4814    ) -> Vec<ConstraintSegment> {
4815        segments
4816            .iter()
4817            .copied()
4818            .filter(|segment| match segment {
4819                ConstraintSegment::Origin(_) => true,
4820                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4821            })
4822            .collect()
4823    }
4824
4825    fn find_referenced_constraints(
4826        &self,
4827        sketch_id: ObjectId,
4828        segment_ids_set: &AhashIndexSet<ObjectId>,
4829    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4830        // Look up the sketch.
4831        let sketch_object = self
4832            .scene_graph
4833            .objects
4834            .get(sketch_id.0)
4835            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4836        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4837            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4838        };
4839        let segment_or_owner_matches = |segment_id: ObjectId| {
4840            if segment_ids_set.contains(&segment_id) {
4841                return true;
4842            }
4843            let segment_object = self.scene_graph.objects.get(segment_id.0);
4844            if let Some(obj) = segment_object
4845                && let ObjectKind::Segment { segment } = &obj.kind
4846            {
4847                match segment {
4848                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4849                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4850                    _ => false,
4851                }
4852            } else {
4853                false
4854            }
4855        };
4856        let mut constraint_ids_set = AhashIndexSet::default();
4857        for constraint_id in &sketch.constraints {
4858            let constraint_object = self
4859                .scene_graph
4860                .objects
4861                .get(constraint_id.0)
4862                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4863            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4864                return Err(KclError::refactor(format!(
4865                    "Object is not a constraint, it is {}",
4866                    constraint_object.kind.human_friendly_kind_with_article()
4867                )));
4868            };
4869            let depends_on_segment = match constraint {
4870                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4871                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4872                Constraint::Fixed(fixed) => fixed
4873                    .points
4874                    .iter()
4875                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4876                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4877                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4878                Constraint::EqualRadius(equal_radius) => {
4879                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4880                }
4881                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4882                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4883                Constraint::Horizontal(h) => match h {
4884                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4885                    Horizontal::Points { points } => points.iter().any(|point| match point {
4886                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4887                        ConstraintSegment::Origin(_) => false,
4888                    }),
4889                },
4890                Constraint::Vertical(v) => match v {
4891                    Vertical::Line { line } => segment_or_owner_matches(*line),
4892                    Vertical::Points { points } => points.iter().any(|point| match point {
4893                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4894                        ConstraintSegment::Origin(_) => false,
4895                    }),
4896                },
4897                Constraint::LinesEqualLength(lines_equal_length) => {
4898                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4899                }
4900                Constraint::Midpoint(midpoint) => {
4901                    segment_or_owner_matches(midpoint.segment)
4902                        || matches!(
4903                            midpoint.point,
4904                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4905                        )
4906                }
4907                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4908                Constraint::Perpendicular(perpendicular) => {
4909                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4910                }
4911                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4912                Constraint::Symmetric(symmetric) => {
4913                    segment_or_owner_matches(symmetric.axis)
4914                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4915                }
4916                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4917            };
4918            if depends_on_segment {
4919                constraint_ids_set.insert(*constraint_id);
4920            }
4921        }
4922        Ok(constraint_ids_set)
4923    }
4924
4925    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4926        let mut outcome = outcome;
4927        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4928        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4929
4930        if freedom_analysis_ran {
4931            // When freedom analysis ran, replace the cache entirely with new values
4932            // Don't merge with old values since IDs might have changed
4933            self.point_freedom_cache.clear();
4934            for new_obj in &new_objects {
4935                if let ObjectKind::Segment {
4936                    segment: kcl_api::Segment::Point(point),
4937                } = &new_obj.kind
4938                {
4939                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4940                }
4941            }
4942            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4943            // Objects are already correct from the analysis, just use them as-is
4944            self.scene_graph.objects = new_objects;
4945        } else {
4946            // When freedom analysis didn't run, preserve old values and merge
4947            // Before replacing objects, extract and store freedom values from old objects
4948            for old_obj in &self.scene_graph.objects {
4949                if let ObjectKind::Segment {
4950                    segment: kcl_api::Segment::Point(point),
4951                } = &old_obj.kind
4952                {
4953                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4954                }
4955            }
4956
4957            // Update objects, preserving stored freedom values when new is Free (might be default)
4958            let mut updated_objects = Vec::with_capacity(new_objects.len());
4959            for new_obj in new_objects {
4960                let mut obj = new_obj;
4961                if let ObjectKind::Segment {
4962                    segment: kcl_api::Segment::Point(point),
4963                } = &mut obj.kind
4964                {
4965                    let new_freedom = point.freedom;
4966                    // When freedom_analysis=false, new values are defaults (Free).
4967                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4968                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4969                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4970                    // when there are actually unsatisfied constraints.
4971                    match new_freedom {
4972                        Freedom::Free => {
4973                            match self.point_freedom_cache.get(&obj.id).copied() {
4974                                Some(Freedom::Conflict) => {
4975                                    // Don't preserve Conflict - conflicts are transient
4976                                    // Keep it as Free
4977                                }
4978                                Some(Freedom::Fixed) => {
4979                                    // Preserve Fixed cached value
4980                                    point.freedom = Freedom::Fixed;
4981                                }
4982                                Some(Freedom::Free) => {
4983                                    // If stored is also Free, keep Free (no change needed)
4984                                }
4985                                None => {
4986                                    // If no cached value, keep Free (default)
4987                                }
4988                            }
4989                        }
4990                        Freedom::Fixed => {
4991                            // Use new value (already set)
4992                        }
4993                        Freedom::Conflict => {
4994                            // Use new value (already set)
4995                        }
4996                    }
4997                    // Store the new freedom value (even if it's Free, so we know it was set)
4998                    self.point_freedom_cache.insert(obj.id, point.freedom);
4999                }
5000                updated_objects.push(obj);
5001            }
5002
5003            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5004            self.scene_graph.objects = updated_objects;
5005        }
5006        outcome
5007    }
5008
5009    fn mutate_ast(
5010        &mut self,
5011        ast: &mut ast::Node<ast::Program>,
5012        object_id: ObjectId,
5013        command: AstMutateCommand,
5014    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5015        let sketch_object = self
5016            .scene_graph
5017            .objects
5018            .get(object_id.0)
5019            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5020        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5021    }
5022
5023    fn mutate_constraint_label_position(
5024        &mut self,
5025        ast: &mut ast::Node<ast::Program>,
5026        constraint_id: ObjectId,
5027        label_position: Point2d<Number>,
5028    ) -> Result<(), KclError> {
5029        let object = self
5030            .scene_graph
5031            .objects
5032            .get(constraint_id.0)
5033            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5034        if !matches!(
5035            &object.kind,
5036            ObjectKind::Constraint {
5037                constraint: Constraint::Distance(_)
5038                    | Constraint::HorizontalDistance(_)
5039                    | Constraint::VerticalDistance(_)
5040                    | Constraint::Radius(_)
5041                    | Constraint::Diameter(_)
5042                    | Constraint::Angle(_),
5043            }
5044        ) {
5045            return Err(KclError::refactor(format!(
5046                "Object does not support labelPosition: {constraint_id:?}"
5047            )));
5048        }
5049
5050        let label_position = to_ast_point2d_number(&label_position)
5051            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5052        self.mutate_ast(
5053            ast,
5054            constraint_id,
5055            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5056        )?;
5057        Ok(())
5058    }
5059}
5060
5061fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5062    // Look up existing sketch.
5063    let sketch_object = scene_graph
5064        .objects
5065        .get(sketch_id.0)
5066        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5067    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5068        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5069    };
5070    expect_single_node_ref(sketch_object)
5071}
5072
5073fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5074    match &object.source {
5075        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5076            range: *range,
5077            node_path: node_path.clone(),
5078        }),
5079        SourceRef::BackTrace { ranges } => {
5080            let [range] = ranges.as_slice() else {
5081                return Err(KclError::refactor(format!(
5082                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5083                    ranges.len()
5084                )));
5085            };
5086            Ok(AstNodeRef {
5087                range: range.0,
5088                node_path: range.1.clone(),
5089            })
5090        }
5091    }
5092}
5093
5094/// This is a deprecated fall-back implementation. Prefer
5095/// [`only_sketch_block()`] to avoid reliance on source ranges.
5096fn only_sketch_block_from_range(
5097    ast: &mut ast::Node<ast::Program>,
5098    sketch_block_range: SourceRange,
5099    edit_kind: ChangeKind,
5100) -> Result<(), KclError> {
5101    let r1 = sketch_block_range;
5102    let matches_range = |r2: SourceRange| -> bool {
5103        // We may have added items to the sketch block, so the end may not be an
5104        // exact match.
5105        match edit_kind {
5106            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5107            // For edit, we don't know whether it grew or shrank.
5108            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5109            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5110            // No edit should be an exact match.
5111            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5112        }
5113    };
5114    let mut found = false;
5115    for item in ast.body.iter_mut() {
5116        match item {
5117            ast::BodyItem::ImportStatement(_) => {}
5118            ast::BodyItem::ExpressionStatement(node) => {
5119                if matches_range(SourceRange::from(&*node))
5120                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5121                {
5122                    sketch_block.is_being_edited = true;
5123                    found = true;
5124                    break;
5125                }
5126            }
5127            ast::BodyItem::VariableDeclaration(node) => {
5128                if matches_range(SourceRange::from(&node.declaration.init))
5129                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5130                {
5131                    sketch_block.is_being_edited = true;
5132                    found = true;
5133                    break;
5134                }
5135            }
5136            ast::BodyItem::TypeDeclaration(_) => {}
5137            ast::BodyItem::ReturnStatement(node) => {
5138                if matches_range(SourceRange::from(&node.argument))
5139                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5140                {
5141                    sketch_block.is_being_edited = true;
5142                    found = true;
5143                    break;
5144                }
5145            }
5146        }
5147    }
5148    if !found {
5149        return Err(KclError::refactor(format!(
5150            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5151        )));
5152    }
5153
5154    Ok(())
5155}
5156
5157fn only_sketch_block(
5158    ast: &mut ast::Node<ast::Program>,
5159    sketch_block_ref: &AstNodeRef,
5160    edit_kind: ChangeKind,
5161) -> Result<(), KclError> {
5162    let Some(target_node_path) = &sketch_block_ref.node_path else {
5163        #[cfg(target_arch = "wasm32")]
5164        web_sys::console::warn_1(
5165            &format!(
5166                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5167                sketch_block_ref
5168            )
5169            .into(),
5170        );
5171        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5172    };
5173    struct MarkSketchBlockBeingEdited<'a> {
5174        target_node_path: &'a ast::NodePath,
5175    }
5176
5177    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5178        type Break = ();
5179        type Continue = ();
5180
5181        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5182            if let NodeMut::SketchBlock(sketch_block) = node
5183                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5184            {
5185                sketch_block.is_being_edited = true;
5186                return TraversalReturn::new_break(());
5187            }
5188            TraversalReturn::new_continue(())
5189        }
5190
5191        fn finish(&mut self, _node: NodeMut<'_>) {}
5192    }
5193
5194    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5195    let found = dfs_mut(ast, &mut marker).is_break();
5196    if !found {
5197        return Err(KclError::refactor(format!(
5198            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5199        )));
5200    }
5201
5202    Ok(())
5203}
5204
5205fn sketch_on_ast_expr(
5206    ast: &mut ast::Node<ast::Program>,
5207    scene_graph: &SceneGraph,
5208    solid_references: &HashMap<Uuid, SolidAstReference>,
5209    on: &Plane,
5210) -> Result<ast::Expr, KclError> {
5211    match on {
5212        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5213        Plane::Object(object_id) => {
5214            let on_object = scene_graph
5215                .objects
5216                .get(object_id.0)
5217                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5218            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5219                return Ok(face_expr);
5220            }
5221            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5222        }
5223        Plane::PrimitiveFace(face) => {
5224            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5225                KclError::refactor(format!(
5226                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5227                    face.solid_id
5228                ))
5229            })?;
5230            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5231            Ok(create_face_of_ast(solid_expr, face_id_expr))
5232        }
5233    }
5234}
5235
5236fn solid_references_from_variables(
5237    ast: &ast::Node<ast::Program>,
5238    variables: &IndexMap<String, KclValueView>,
5239) -> HashMap<Uuid, SolidAstReference> {
5240    let mut references = HashMap::new();
5241
5242    // In-place operations such as shell reuse their input solid's engine ID.
5243    // Later variables overwrite earlier references so mutations target the result.
5244    for item in &ast.body {
5245        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5246            continue;
5247        };
5248        let name = &declaration.declaration.id.name;
5249        let Some(value) = variables.get(name) else {
5250            continue;
5251        };
5252
5253        match value {
5254            KclValueView::Solid { value } => {
5255                references.insert(
5256                    value.id,
5257                    SolidAstReference {
5258                        variable_name: name.clone(),
5259                        output_index: None,
5260                    },
5261                );
5262            }
5263            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5264                for (output_index, entry) in value.iter().enumerate() {
5265                    if let KclValueView::Solid { value } = entry {
5266                        references.insert(
5267                            value.id,
5268                            SolidAstReference {
5269                                variable_name: name.clone(),
5270                                output_index: Some(output_index),
5271                            },
5272                        );
5273                    }
5274                }
5275            }
5276            _ => {}
5277        }
5278    }
5279
5280    references
5281}
5282
5283fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5284    let reference = solid_references.get(&solid_id)?;
5285    let solid_expr = ast_name_expr(reference.variable_name.clone());
5286    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5287}
5288
5289fn sketch_face_of_scene_object_ast_expr(
5290    ast: &mut ast::Node<ast::Program>,
5291    on_object: &kcl_api::Object,
5292) -> Result<Option<ast::Expr>, KclError> {
5293    match &on_object.kind {
5294        ObjectKind::Wall(wall) => {
5295            let solid_ref = get_or_insert_ast_reference(
5296                ast,
5297                &source_ref_from_source_ref_range(&wall.source.solid),
5298                "solid",
5299                None,
5300            )?;
5301            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5302                return Err(KclError::refactor(format!(
5303                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5304                    on_object.artifact_id
5305                )));
5306            };
5307            let solid_expr = indexed_solid_expr_for_sweep_output(
5308                ast_name_expr(solid_name_expr.name.name.clone()),
5309                wall.solid_output_index,
5310            );
5311            let sweep_ref = get_or_insert_ast_reference(
5312                ast,
5313                &source_ref_from_source_ref_range(&wall.source.sweep),
5314                "solid",
5315                None,
5316            )?;
5317            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5318                return Err(KclError::refactor(format!(
5319                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5320                    on_object.artifact_id
5321                )));
5322            };
5323            let sweep_name = sweep_name_expr.name.name.clone();
5324            let segment_ref = get_or_insert_ast_reference(
5325                ast,
5326                &source_ref_from_source_ref_range(&wall.source.segment),
5327                LINE_VARIABLE,
5328                None,
5329            )?;
5330
5331            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5332                wall.source
5333                    .path
5334                    .as_ref()
5335                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5336            }) {
5337                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5338                    return Err(KclError::refactor(format!(
5339                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5340                        on_object.artifact_id
5341                    )));
5342                };
5343                create_member_expression(
5344                    create_member_expression(ast_name_expr(region_name), "tags"),
5345                    &segment_name_expr.name.name,
5346                )
5347            } else {
5348                segment_ref
5349            };
5350
5351            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5352        }
5353        ObjectKind::Cap(cap) => {
5354            let solid_ref =
5355                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5356            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5357                return Err(KclError::refactor(format!(
5358                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5359                    on_object.artifact_id
5360                )));
5361            };
5362            let solid_expr = indexed_solid_expr_for_sweep_output(
5363                ast_name_expr(solid_name_expr.name.name.clone()),
5364                cap.solid_output_index,
5365            );
5366            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5367            let face_expr = match cap.kind {
5368                kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5369                kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5370            };
5371
5372            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5373        }
5374        _ => Ok(None),
5375    }
5376}
5377
5378fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5379    match solid_output_index {
5380        Some(output_index) => create_index_expression(solid_expr, output_index),
5381        None => solid_expr,
5382    }
5383}
5384
5385fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5386    SourceRef::Simple {
5387        range: source.range,
5388        node_path: source.node_path.clone(),
5389    }
5390}
5391
5392fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5393    let source_ref = source_ref_from_source_ref_range(path_source);
5394    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5395    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5396        return None;
5397    };
5398    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5399        return None;
5400    };
5401    if region_call.callee.name.name != "region" {
5402        return None;
5403    }
5404    Some(candidate)
5405}
5406
5407fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5408    let mut current_id = source_id;
5409    let mut current_composite = None;
5410    let mut visited = HashSet::new();
5411
5412    while visited.insert(current_id) {
5413        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5414
5415        let Some(composite_id) = next_composite_id else {
5416            break;
5417        };
5418        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5419            break;
5420        };
5421
5422        current_id = composite.id;
5423        current_composite = Some(composite);
5424
5425        if !composite.consumed {
5426            break;
5427        }
5428    }
5429
5430    current_composite.map(|composite| &composite.code_ref)
5431}
5432
5433fn downstream_composite_id_for_solid_source(
5434    artifact_graph: &ArtifactGraph,
5435    source_id: ArtifactId,
5436) -> Option<ArtifactId> {
5437    // Source is a path, find its solid.
5438    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5439        && let Some(composite_id) = path.composite_solid_id
5440        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5441        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5442    {
5443        return Some(composite_id);
5444    }
5445
5446    // Source is a sweep, find its path -> then find the solid
5447    for artifact in artifact_graph.values() {
5448        if let Artifact::Path(path) = artifact
5449            && path.sweep_id == Some(source_id)
5450            && let Some(composite_id) = path.composite_solid_id
5451            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5452            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5453        {
5454            return Some(composite_id);
5455        }
5456    }
5457
5458    // Source is a solid, find its downstream solid.
5459    artifact_graph.values().find_map(|artifact| {
5460        let Artifact::CompositeSolid(composite) = artifact else {
5461            return None;
5462        };
5463        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5464    })
5465}
5466
5467fn composite_contains_path_input(
5468    solid_ids: &[ArtifactId],
5469    tool_ids: &[ArtifactId],
5470    path_id: ArtifactId,
5471    solid2d_id: Option<ArtifactId>,
5472) -> bool {
5473    composite_contains_input(solid_ids, tool_ids, path_id)
5474        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5475}
5476
5477fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5478    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5479}
5480
5481fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5482    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5483    SourceRefRange {
5484        range: code_ref.range,
5485        node_path,
5486    }
5487}
5488
5489fn solid_output_index_for_sweep(
5490    artifact_graph: &ArtifactGraph,
5491    sweep_id: ArtifactId,
5492    sweep_code_ref: &CodeRef,
5493) -> Option<usize> {
5494    // Constituent sweeps are implementation details of one composite body,
5495    // even when cloning gives them the same CodeRef as the composite root.
5496    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5497        return None;
5498    }
5499
5500    let sibling_sweeps = artifact_graph
5501        .values()
5502        .filter_map(|artifact| match artifact {
5503            Artifact::Sweep(sweep)
5504                if sweep.code_ref.range == sweep_code_ref.range
5505                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5506            {
5507                Some(sweep)
5508            }
5509            _ => None,
5510        })
5511        .collect::<Vec<_>>();
5512
5513    if sibling_sweeps.len() <= 1 {
5514        return None;
5515    }
5516
5517    sibling_sweeps
5518        .iter()
5519        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5520}
5521
5522fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5523    let mut existing_artifact_ids = scene_objects
5524        .iter()
5525        .map(|object| object.artifact_id)
5526        .collect::<HashSet<_>>();
5527
5528    for artifact in artifact_graph.values() {
5529        match artifact {
5530            Artifact::Wall(wall) => {
5531                if existing_artifact_ids.contains(&wall.id) {
5532                    continue;
5533                }
5534
5535                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5536                    Artifact::Segment(segment) => Some(segment),
5537                    _ => None,
5538                }) else {
5539                    continue;
5540                };
5541                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5542                    Artifact::Sweep(sweep) => Some(sweep),
5543                    _ => None,
5544                }) else {
5545                    continue;
5546                };
5547                let source_segment = segment
5548                    .original_seg_id
5549                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5550                    .and_then(|artifact| match artifact {
5551                        Artifact::Segment(segment) => Some(segment),
5552                        _ => None,
5553                    })
5554                    .unwrap_or(segment);
5555                let solid_code_ref =
5556                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5557                let path_code_ref = artifact_graph
5558                    .get(&segment.path_id)
5559                    .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5560                    .and_then(|artifact| match artifact {
5561                        Artifact::Path(path) => Some(&path.code_ref),
5562                        _ => None,
5563                    });
5564                let source = WallSource {
5565                    solid: code_ref_source_ref_range(solid_code_ref),
5566                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5567                    path: path_code_ref.map(code_ref_source_ref_range),
5568                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5569                };
5570                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5571                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5572                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5573                    .flatten();
5574                let object_source = source_ref_from_source_ref_range(&source.solid);
5575                let id = ObjectId(scene_objects.len());
5576                scene_objects.push(kcl_api::Object {
5577                    id,
5578                    kind: ObjectKind::Wall(kcl_api::Wall {
5579                        id,
5580                        source,
5581                        solid_output_index,
5582                    }),
5583                    label: Default::default(),
5584                    comments: Default::default(),
5585                    artifact_id: wall.id,
5586                    source: object_source,
5587                });
5588                existing_artifact_ids.insert(wall.id);
5589            }
5590            Artifact::Cap(cap) => {
5591                if existing_artifact_ids.contains(&cap.id) {
5592                    continue;
5593                }
5594
5595                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5596                    Artifact::Sweep(sweep) => Some(sweep),
5597                    _ => None,
5598                }) else {
5599                    continue;
5600                };
5601                let id = ObjectId(scene_objects.len());
5602                let kind = match cap.sub_type {
5603                    CapSubType::Start => kcl_api::CapKind::Start,
5604                    CapSubType::End => kcl_api::CapKind::End,
5605                };
5606                let solid_code_ref =
5607                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5608                let source = CapSource {
5609                    solid: code_ref_source_ref_range(solid_code_ref),
5610                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5611                };
5612                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5613                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5614                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5615                    .flatten();
5616                let object_source = source_ref_from_source_ref_range(&source.solid);
5617                scene_objects.push(kcl_api::Object {
5618                    id,
5619                    kind: ObjectKind::Cap(kcl_api::Cap {
5620                        id,
5621                        kind,
5622                        source,
5623                        solid_output_index,
5624                    }),
5625                    label: Default::default(),
5626                    comments: Default::default(),
5627                    artifact_id: cap.id,
5628                    source: object_source,
5629                });
5630                existing_artifact_ids.insert(cap.id);
5631            }
5632            _ => {}
5633        }
5634    }
5635}
5636
5637fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5638    use crate::engine::PlaneName;
5639
5640    match name {
5641        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5642        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5643        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5644        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5645        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5646        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5647    }
5648}
5649
5650fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5651    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5652        ast::UnaryOperator::Neg,
5653        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5654    )))
5655}
5656
5657fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5658    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5660        unlabeled: Some(solid_expr),
5661        arguments: vec![ast::LabeledArg {
5662            label: Some(ast::Identifier::new("face")),
5663            arg: face_expr,
5664        }],
5665        digest: None,
5666        non_code_meta: Default::default(),
5667    })))
5668}
5669
5670fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5671    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5672        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5673        unlabeled: Some(solid_expr),
5674        arguments: vec![ast::LabeledArg {
5675            label: Some(ast::Identifier::new("index")),
5676            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5677                ast::NumericLiteral {
5678                    value: index as f64,
5679                    suffix: NumericSuffix::None,
5680                    raw: index.to_string(),
5681                    digest: None,
5682                },
5683            )))),
5684        }],
5685        digest: None,
5686        non_code_meta: Default::default(),
5687    })))
5688}
5689
5690fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5691    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5692        return None;
5693    };
5694    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5695        return None;
5696    };
5697    if !matches!(
5698        sweep_call.callee.name.name.as_str(),
5699        "extrude" | "revolve" | "sweep" | "loft"
5700    ) {
5701        return None;
5702    }
5703    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5704        return None;
5705    };
5706    let candidate = region_name_expr.name.name.clone();
5707    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5708        return None;
5709    };
5710    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5711        return None;
5712    };
5713    if region_call.callee.name.name != "region" {
5714        return None;
5715    }
5716    Some(candidate)
5717}
5718
5719/// Return the AST expression referencing the variable at the given source ref.
5720/// If no such variable exists, insert a new variable declaration with the given
5721/// prefix.
5722///
5723/// This may return a complex expression referencing properties of the variable
5724/// (e.g., `line1.start`).
5725fn get_or_insert_ast_reference(
5726    ast: &mut ast::Node<ast::Program>,
5727    source_ref: &SourceRef,
5728    prefix: &str,
5729    property: Option<&str>,
5730) -> Result<ast::Expr, KclError> {
5731    let command = AstMutateCommand::AddVariableDeclaration {
5732        prefix: prefix.to_owned(),
5733    };
5734    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5735        Ok((_, ret)) => ret,
5736        Err(err) => {
5737            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5738                AstMutateCommandReturn::Name(var_name)
5739            } else {
5740                return Err(err);
5741            }
5742        }
5743    };
5744    let AstMutateCommandReturn::Name(var_name) = ret else {
5745        return Err(KclError::refactor(
5746            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5747        ));
5748    };
5749    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5750    let Some(property) = property else {
5751        // No property; just return the variable name.
5752        return Ok(var_expr);
5753    };
5754
5755    Ok(create_member_expression(var_expr, property))
5756}
5757
5758fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5759    let source_range = match source_ref {
5760        SourceRef::Simple { range, .. } => *range,
5761        SourceRef::BackTrace { ranges } => {
5762            let [range] = ranges.as_slice() else {
5763                return None;
5764            };
5765            range.0
5766        }
5767    };
5768    ast.body.iter().find_map(|item| {
5769        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5770            return None;
5771        };
5772        let init_range = SourceRange::from(&var_decl.declaration.init);
5773        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5774            && init_range.start() <= source_range.start()
5775            && source_range.end() <= init_range.end();
5776        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5777            && init_range != source_range
5778            && source_is_inside_init
5779        {
5780            return None;
5781        }
5782        source_is_inside_init.then(|| var_decl.name().to_owned())
5783    })
5784}
5785
5786fn mutate_ast_node_by_source_ref(
5787    ast: &mut ast::Node<ast::Program>,
5788    source_ref: &SourceRef,
5789    command: AstMutateCommand,
5790) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5791    let (source_range, node_path) = match source_ref {
5792        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5793        SourceRef::BackTrace { ranges } => {
5794            let [range] = ranges.as_slice() else {
5795                return Err(KclError::refactor(format!(
5796                    "Expected single source ref, got {}; ranges={ranges:#?}",
5797                    ranges.len(),
5798                )));
5799            };
5800            (range.0, range.1.clone())
5801        }
5802    };
5803    let mut context = AstMutateContext {
5804        source_range,
5805        node_path,
5806        command,
5807        defined_names_stack: Default::default(),
5808    };
5809    let control = dfs_mut(ast, &mut context);
5810    match control {
5811        ControlFlow::Continue(_) => Err(KclError::refactor(
5812            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5813        )),
5814        ControlFlow::Break(break_value) => break_value,
5815    }
5816}
5817
5818#[derive(Debug)]
5819struct AstMutateContext {
5820    source_range: SourceRange,
5821    node_path: Option<ast::NodePath>,
5822    command: AstMutateCommand,
5823    defined_names_stack: Vec<HashSet<String>>,
5824}
5825
5826#[derive(Debug)]
5827#[allow(clippy::large_enum_variant)]
5828enum AstMutateCommand {
5829    /// Add an expression statement to the sketch block.
5830    AddSketchBlockExprStmt {
5831        expr: ast::Expr,
5832    },
5833    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5834    AddSketchBlockVarDecl {
5835        prefix: String,
5836        expr: ast::Expr,
5837    },
5838    AddVariableDeclaration {
5839        prefix: String,
5840    },
5841    EditPoint {
5842        at: ast::Expr,
5843    },
5844    EditLine {
5845        start: ast::Expr,
5846        end: ast::Expr,
5847        construction: Option<bool>,
5848    },
5849    EditArc {
5850        start: ast::Expr,
5851        end: ast::Expr,
5852        center: ast::Expr,
5853        direction: Option<ArcDirection>,
5854        construction: Option<bool>,
5855    },
5856    EditCircle {
5857        start: ast::Expr,
5858        center: ast::Expr,
5859        construction: Option<bool>,
5860    },
5861    EditControlPointSpline {
5862        points: ast::Expr,
5863        construction: Option<bool>,
5864    },
5865    EditConstraintValue {
5866        value: ast::BinaryPart,
5867    },
5868    EditAngleConstraint {
5869        call: ast::BinaryPart,
5870        value: ast::BinaryPart,
5871    },
5872    EditDistanceConstraint {
5873        call: ast::BinaryPart,
5874        value: ast::BinaryPart,
5875    },
5876    EditDistanceConstraintLabelPosition {
5877        label_position: ast::Expr,
5878    },
5879    EditCallUnlabeled {
5880        arg: ast::Expr,
5881    },
5882    EditVarInitialValue {
5883        value: Number,
5884    },
5885    DeleteNode,
5886}
5887
5888impl AstMutateCommand {
5889    fn needs_defined_names_stack(&self) -> bool {
5890        matches!(
5891            self,
5892            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5893        )
5894    }
5895}
5896
5897#[derive(Debug)]
5898enum AstMutateCommandReturn {
5899    None,
5900    Name(String),
5901}
5902
5903#[derive(Debug, Clone)]
5904struct AstNodeRef {
5905    range: SourceRange,
5906    node_path: Option<ast::NodePath>,
5907}
5908
5909impl<T> From<&ast::Node<T>> for AstNodeRef {
5910    fn from(value: &ast::Node<T>) -> Self {
5911        AstNodeRef {
5912            range: value.into(),
5913            node_path: value.node_path.clone(),
5914        }
5915    }
5916}
5917
5918impl From<&ast::BodyItem> for AstNodeRef {
5919    fn from(value: &ast::BodyItem) -> Self {
5920        match value {
5921            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5922                range: node.into(),
5923                node_path: node.node_path.clone(),
5924            },
5925            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5926                range: node.into(),
5927                node_path: node.node_path.clone(),
5928            },
5929            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5930                range: node.into(),
5931                node_path: node.node_path.clone(),
5932            },
5933            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5934                range: node.into(),
5935                node_path: node.node_path.clone(),
5936            },
5937            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5938                range: node.into(),
5939                node_path: node.node_path.clone(),
5940            },
5941        }
5942    }
5943}
5944
5945impl From<&ast::Expr> for AstNodeRef {
5946    fn from(value: &ast::Expr) -> Self {
5947        AstNodeRef {
5948            range: SourceRange::from(value),
5949            node_path: value.node_path().cloned(),
5950        }
5951    }
5952}
5953
5954impl From<&AstMutateContext> for AstNodeRef {
5955    fn from(value: &AstMutateContext) -> Self {
5956        AstNodeRef {
5957            range: value.source_range,
5958            node_path: value.node_path.clone(),
5959        }
5960    }
5961}
5962
5963impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5964    type Error = crate::walk::AstNodeError;
5965
5966    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5967        Ok(AstNodeRef {
5968            range: SourceRange::try_from(value)?,
5969            node_path: value.try_into()?,
5970        })
5971    }
5972}
5973
5974impl From<AstNodeRef> for SourceRange {
5975    fn from(value: AstNodeRef) -> Self {
5976        value.range
5977    }
5978}
5979
5980impl Visitor for AstMutateContext {
5981    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5982    type Continue = ();
5983
5984    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5985        filter_and_process(self, node)
5986    }
5987
5988    fn finish(&mut self, node: NodeMut<'_>) {
5989        match &node {
5990            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5991                self.defined_names_stack.pop();
5992            }
5993            _ => {}
5994        }
5995    }
5996}
5997
5998fn filter_and_process(
5999    ctx: &mut AstMutateContext,
6000    node: NodeMut,
6001) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
6002    let Ok(node_range) = SourceRange::try_from(&node) else {
6003        // Nodes that can't be converted to a range aren't interesting.
6004        return TraversalReturn::new_continue(());
6005    };
6006    // If we're adding a variable declaration, we need to look at variable
6007    // declaration expressions to see if it already has a variable, before
6008    // continuing. The variable declaration's source range won't match the
6009    // target; its init expression will.
6010    if let NodeMut::VariableDeclaration(var_decl) = &node {
6011        let expr_range = SourceRange::from(&var_decl.declaration.init);
6012        let expr_node_path = var_decl.declaration.init.node_path();
6013        if source_ref_matches(ctx, expr_range, expr_node_path) {
6014            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6015                // We found the variable declaration expression. It doesn't need
6016                // to be added.
6017                return TraversalReturn::new_break(Ok((
6018                    AstNodeRef::from(&**var_decl),
6019                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6020                )));
6021            }
6022            if let AstMutateCommand::DeleteNode = &ctx.command {
6023                // We found the variable declaration. Delete the variable along
6024                // with the segment.
6025                return TraversalReturn {
6026                    mutate_body_item: MutateBodyItem::Delete,
6027                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6028                };
6029            }
6030        }
6031    }
6032    // Similar thing with expression statement. We need to look at the
6033    // expression inside it.
6034    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6035        let expr_range = SourceRange::from(&expr_stmt.expression);
6036        let expr_node_path = expr_stmt.expression.node_path();
6037        if source_ref_matches(ctx, expr_range, expr_node_path) {
6038            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6039                // We found the node wrapped in an expression statement. Process
6040                // the statement.
6041                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6042                    return TraversalReturn::new_continue(());
6043                };
6044                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6045            }
6046            if let AstMutateCommand::DeleteNode = &ctx.command {
6047                // We found the node wrapped in an expression statement. Delete
6048                // the whole statement.
6049                return TraversalReturn {
6050                    mutate_body_item: MutateBodyItem::Delete,
6051                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6052                };
6053            }
6054        }
6055    }
6056
6057    if ctx.command.needs_defined_names_stack() {
6058        if let NodeMut::Program(program) = &node {
6059            ctx.defined_names_stack.push(find_defined_names(*program));
6060        } else if let NodeMut::SketchBlock(block) = &node {
6061            ctx.defined_names_stack.push(find_defined_names(&block.body));
6062        }
6063    }
6064
6065    // Make sure the node matches the source ref.
6066    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6067    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6068        return TraversalReturn::new_continue(());
6069    }
6070    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6071        return TraversalReturn::new_continue(());
6072    };
6073    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6074}
6075
6076fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6077    match &ctx.node_path {
6078        Some(target) => Some(target) == node_path,
6079        None => node_range == ctx.source_range,
6080    }
6081}
6082
6083fn is_angle_constraint_call_name(name: &str) -> bool {
6084    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6085}
6086
6087fn is_distance_constraint_call_name(name: &str) -> bool {
6088    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6089}
6090
6091fn is_constraint_call_name(name: &str) -> bool {
6092    matches!(
6093        name,
6094        DISTANCE_FN
6095            | HORIZONTAL_DISTANCE_FN
6096            | VERTICAL_DISTANCE_FN
6097            | RADIUS_FN
6098            | DIAMETER_FN
6099            | ANGLE_FN
6100            | ANGLE_DIMENSION_FN
6101    )
6102}
6103
6104fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6105    if let ast::BinaryPart::CallExpressionKw(call) = part
6106        && is_constraint_call_name(call.callee.name.name.as_str())
6107    {
6108        Some(call)
6109    } else {
6110        None
6111    }
6112}
6113
6114fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6115    match &ctx.command {
6116        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6117            if let NodeMut::SketchBlock(sketch_block) = node {
6118                sketch_block
6119                    .body
6120                    .items
6121                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6122                        inner: ast::ExpressionStatement {
6123                            expression: expr.clone(),
6124                            digest: None,
6125                        },
6126                        start: Default::default(),
6127                        end: Default::default(),
6128                        module_id: Default::default(),
6129                        node_path: None,
6130                        outer_attrs: Default::default(),
6131                        pre_comments: Default::default(),
6132                        comment_start: Default::default(),
6133                    }));
6134                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6135            }
6136        }
6137        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6138            if let NodeMut::SketchBlock(sketch_block) = node {
6139                let empty_defined_names = HashSet::new();
6140                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6141                let Ok(name) = next_free_name(prefix, defined_names) else {
6142                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6143                };
6144                sketch_block
6145                    .body
6146                    .items
6147                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6148                        ast::VariableDeclaration::new(
6149                            ast::VariableDeclarator::new(&name, expr.clone()),
6150                            ast::ItemVisibility::Default,
6151                            ast::VariableKind::Const,
6152                        ),
6153                    ))));
6154                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6155            }
6156        }
6157        AstMutateCommand::AddVariableDeclaration { prefix } => {
6158            if let NodeMut::VariableDeclaration(inner) = node {
6159                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6160            }
6161            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6162                let empty_defined_names = HashSet::new();
6163                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6164                let Ok(name) = next_free_name(prefix, defined_names) else {
6165                    // TODO: Return an error instead?
6166                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6167                };
6168                let mutate_node =
6169                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6170                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6171                        ast::ItemVisibility::Default,
6172                        ast::VariableKind::Const,
6173                    ))));
6174                return TraversalReturn {
6175                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6176                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6177                };
6178            }
6179        }
6180        AstMutateCommand::EditPoint { at } => {
6181            if let NodeMut::CallExpressionKw(call) = node {
6182                if call.callee.name.name != POINT_FN {
6183                    return TraversalReturn::new_continue(());
6184                }
6185                // Update the arguments.
6186                for labeled_arg in &mut call.arguments {
6187                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6188                        labeled_arg.arg = at.clone();
6189                    }
6190                }
6191                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6192            }
6193        }
6194        AstMutateCommand::EditLine {
6195            start,
6196            end,
6197            construction,
6198        } => {
6199            if let NodeMut::CallExpressionKw(call) = node {
6200                if call.callee.name.name != LINE_FN {
6201                    return TraversalReturn::new_continue(());
6202                }
6203                // Update the arguments.
6204                for labeled_arg in &mut call.arguments {
6205                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6206                        labeled_arg.arg = start.clone();
6207                    }
6208                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6209                        labeled_arg.arg = end.clone();
6210                    }
6211                }
6212                // Handle construction kwarg
6213                if let Some(construction_value) = construction {
6214                    let construction_exists = call
6215                        .arguments
6216                        .iter()
6217                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6218                    if *construction_value {
6219                        // Add or update construction=true
6220                        if construction_exists {
6221                            // Update existing construction kwarg
6222                            for labeled_arg in &mut call.arguments {
6223                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6224                                    labeled_arg.arg =
6225                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6226                                            value: ast::LiteralValue::Bool(true),
6227                                            raw: "true".to_string(),
6228                                            digest: None,
6229                                        })));
6230                                }
6231                            }
6232                        } else {
6233                            // Add new construction kwarg
6234                            call.arguments.push(ast::LabeledArg {
6235                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6236                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6237                                    value: ast::LiteralValue::Bool(true),
6238                                    raw: "true".to_string(),
6239                                    digest: None,
6240                                }))),
6241                            });
6242                        }
6243                    } else {
6244                        // Remove construction kwarg if it exists
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::EditArc {
6253            start,
6254            end,
6255            center,
6256            direction,
6257            construction,
6258        } => {
6259            if let NodeMut::CallExpressionKw(call) = node {
6260                if call.callee.name.name != ARC_FN {
6261                    return TraversalReturn::new_continue(());
6262                }
6263                // Update the arguments.
6264                for labeled_arg in &mut call.arguments {
6265                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6266                        labeled_arg.arg = start.clone();
6267                    }
6268                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6269                        labeled_arg.arg = end.clone();
6270                    }
6271                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6272                        labeled_arg.arg = center.clone();
6273                    }
6274                }
6275                // Handle direction kwarg
6276                if let Some(direction_value) = direction {
6277                    let direction_exists = call
6278                        .arguments
6279                        .iter()
6280                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6281                    if direction_value.is_clockwise() {
6282                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6283                        if direction_exists {
6284                            // Update existing direction kwarg
6285                            for labeled_arg in &mut call.arguments {
6286                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6287                                    labeled_arg.arg = direction_ast.clone();
6288                                }
6289                            }
6290                        } else {
6291                            // Add new direction kwarg
6292                            call.arguments.push(ast::LabeledArg {
6293                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6294                                arg: direction_ast,
6295                            });
6296                        }
6297                    } else {
6298                        // Remove direction kwarg if it exists since
6299                        // counterclockwise is the default
6300                        call.arguments
6301                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6302                    }
6303                }
6304                // Handle construction kwarg
6305                if let Some(construction_value) = construction {
6306                    let construction_exists = call
6307                        .arguments
6308                        .iter()
6309                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6310                    if *construction_value {
6311                        // Add or update construction=true
6312                        if construction_exists {
6313                            // Update existing construction kwarg
6314                            for labeled_arg in &mut call.arguments {
6315                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6316                                    labeled_arg.arg =
6317                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6318                                            value: ast::LiteralValue::Bool(true),
6319                                            raw: "true".to_string(),
6320                                            digest: None,
6321                                        })));
6322                                }
6323                            }
6324                        } else {
6325                            // Add new construction kwarg
6326                            call.arguments.push(ast::LabeledArg {
6327                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6328                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6329                                    value: ast::LiteralValue::Bool(true),
6330                                    raw: "true".to_string(),
6331                                    digest: None,
6332                                }))),
6333                            });
6334                        }
6335                    } else {
6336                        // Remove construction kwarg if it exists
6337                        call.arguments
6338                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6339                    }
6340                }
6341                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6342            }
6343        }
6344        AstMutateCommand::EditCircle {
6345            start,
6346            center,
6347            construction,
6348        } => {
6349            if let NodeMut::CallExpressionKw(call) = node {
6350                if call.callee.name.name != CIRCLE_FN {
6351                    return TraversalReturn::new_continue(());
6352                }
6353                // Update the arguments.
6354                for labeled_arg in &mut call.arguments {
6355                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6356                        labeled_arg.arg = start.clone();
6357                    }
6358                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6359                        labeled_arg.arg = center.clone();
6360                    }
6361                }
6362                // Handle construction kwarg
6363                if let Some(construction_value) = construction {
6364                    let construction_exists = call
6365                        .arguments
6366                        .iter()
6367                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6368                    if *construction_value {
6369                        if construction_exists {
6370                            // Update existing construction kwarg
6371                            for labeled_arg in &mut call.arguments {
6372                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6373                                    labeled_arg.arg =
6374                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6375                                            value: ast::LiteralValue::Bool(true),
6376                                            raw: "true".to_string(),
6377                                            digest: None,
6378                                        })));
6379                                }
6380                            }
6381                        } else {
6382                            // Add new construction kwarg
6383                            call.arguments.push(ast::LabeledArg {
6384                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6385                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6386                                    value: ast::LiteralValue::Bool(true),
6387                                    raw: "true".to_string(),
6388                                    digest: None,
6389                                }))),
6390                            });
6391                        }
6392                    } else {
6393                        // Remove construction kwarg if it exists
6394                        call.arguments
6395                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6396                    }
6397                }
6398                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6399            }
6400        }
6401        AstMutateCommand::EditControlPointSpline { points, construction } => {
6402            if let NodeMut::CallExpressionKw(call) = node {
6403                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6404                    return TraversalReturn::new_continue(());
6405                }
6406                for labeled_arg in &mut call.arguments {
6407                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6408                    {
6409                        labeled_arg.arg = points.clone();
6410                    }
6411                }
6412                // Handle construction kwarg
6413                if let Some(construction_value) = construction {
6414                    let construction_exists = call
6415                        .arguments
6416                        .iter()
6417                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6418                    if *construction_value {
6419                        if construction_exists {
6420                            for labeled_arg in &mut call.arguments {
6421                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6422                                    labeled_arg.arg =
6423                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6424                                            value: ast::LiteralValue::Bool(true),
6425                                            raw: "true".to_string(),
6426                                            digest: None,
6427                                        })));
6428                                }
6429                            }
6430                        } else {
6431                            call.arguments.push(ast::LabeledArg {
6432                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6433                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6434                                    value: ast::LiteralValue::Bool(true),
6435                                    raw: "true".to_string(),
6436                                    digest: None,
6437                                }))),
6438                            });
6439                        }
6440                    } else {
6441                        call.arguments
6442                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6443                    }
6444                }
6445                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446            }
6447        }
6448        AstMutateCommand::EditConstraintValue { value } => {
6449            if let NodeMut::BinaryExpression(binary_expr) = node {
6450                let left_is_constraint = matches!(
6451                    &binary_expr.left,
6452                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6453                );
6454                if left_is_constraint {
6455                    binary_expr.right = value.clone();
6456                } else {
6457                    binary_expr.left = value.clone();
6458                }
6459
6460                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6461            }
6462        }
6463        AstMutateCommand::EditAngleConstraint { call, value } => {
6464            if let NodeMut::BinaryExpression(binary_expr) = node {
6465                let left_is_angle = matches!(
6466                    &binary_expr.left,
6467                    ast::BinaryPart::CallExpressionKw(existing_call)
6468                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6469                );
6470                let right_is_angle = matches!(
6471                    &binary_expr.right,
6472                    ast::BinaryPart::CallExpressionKw(existing_call)
6473                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6474                );
6475
6476                match (left_is_angle, right_is_angle) {
6477                    (true, _) => {
6478                        binary_expr.left = call.clone();
6479                        binary_expr.right = value.clone();
6480                    }
6481                    (false, true) => {
6482                        binary_expr.left = value.clone();
6483                        binary_expr.right = call.clone();
6484                    }
6485                    (false, false) => return TraversalReturn::new_continue(()),
6486                }
6487
6488                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6489            }
6490        }
6491        AstMutateCommand::EditDistanceConstraint { call, value } => {
6492            if let NodeMut::BinaryExpression(binary_expr) = node {
6493                let left_is_distance = matches!(
6494                    &binary_expr.left,
6495                    ast::BinaryPart::CallExpressionKw(existing_call)
6496                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6497                );
6498                let right_is_distance = matches!(
6499                    &binary_expr.right,
6500                    ast::BinaryPart::CallExpressionKw(existing_call)
6501                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6502                );
6503
6504                match (left_is_distance, right_is_distance) {
6505                    (true, _) => {
6506                        binary_expr.left = call.clone();
6507                        binary_expr.right = value.clone();
6508                    }
6509                    (false, true) => {
6510                        binary_expr.left = value.clone();
6511                        binary_expr.right = call.clone();
6512                    }
6513                    (false, false) => return TraversalReturn::new_continue(()),
6514                }
6515
6516                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6517            }
6518        }
6519        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6520            if let NodeMut::BinaryExpression(binary_expr) = node {
6521                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6522                    call
6523                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6524                    call
6525                } else {
6526                    return TraversalReturn::new_continue(());
6527                };
6528
6529                if let Some(label_arg) = call
6530                    .arguments
6531                    .iter_mut()
6532                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6533                {
6534                    label_arg.arg = label_position.clone();
6535                } else {
6536                    call.arguments.push(ast::LabeledArg {
6537                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6538                        arg: label_position.clone(),
6539                    });
6540                }
6541
6542                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6543            }
6544        }
6545        AstMutateCommand::EditCallUnlabeled { arg } => {
6546            if let NodeMut::CallExpressionKw(call) = node {
6547                call.unlabeled = Some(arg.clone());
6548                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549            }
6550        }
6551        AstMutateCommand::EditVarInitialValue { value } => {
6552            // We target the SketchVar itself (matched by NodePath) rather than
6553            // the inner NumericLiteral so we can also write back into vars that
6554            // were declared without an initial value (e.g. bare `var`).
6555            if let NodeMut::SketchVar(sketch_var) = node {
6556                let Ok(literal) = to_source_number(*value) else {
6557                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6558                        "Could not convert number to AST literal: {:?}",
6559                        *value
6560                    ))));
6561                };
6562                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6563                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6564            }
6565        }
6566        AstMutateCommand::DeleteNode => {
6567            return TraversalReturn {
6568                mutate_body_item: MutateBodyItem::Delete,
6569                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6570            };
6571        }
6572    }
6573    TraversalReturn::new_continue(())
6574}
6575
6576struct FindSketchBlockSourceRange {
6577    /// The source range of the sketch block before mutation.
6578    target_before_mutation: SourceRange,
6579    /// The source range of the sketch block's last body item after mutation. We
6580    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6581    /// shared reference.
6582    found: Cell<Option<AstNodeRef>>,
6583}
6584
6585impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6586    type Error = crate::front::Error;
6587
6588    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6589        let Ok(node_range) = SourceRange::try_from(&node) else {
6590            return Ok(true);
6591        };
6592
6593        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6594            if node_range.module_id() == self.target_before_mutation.module_id()
6595                && node_range.start() == self.target_before_mutation.start()
6596                // End shouldn't match since we added something.
6597                && node_range.end() >= self.target_before_mutation.end()
6598            {
6599                self.found.set(sketch_block.body.items.last().map(|item| match item {
6600                    // For declarations like `circle1 = circle(...)`, use
6601                    // the init expression range so lookup in source_range_to_object
6602                    // matches the segment source range.
6603                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6604                    _ => AstNodeRef::from(item),
6605                }));
6606                return Ok(false);
6607            } else {
6608                // We found a different sketch block. No need to descend into
6609                // its children since sketch blocks cannot be nested.
6610                return Ok(true);
6611            }
6612        }
6613
6614        for child in node.children().iter() {
6615            if !child.visit(*self)? {
6616                return Ok(false);
6617            }
6618        }
6619
6620        Ok(true)
6621    }
6622}
6623
6624struct FindSketchBlockByNodePath {
6625    /// The Node Path of the sketch block before mutation.
6626    target_node_path: ast::NodePath,
6627    /// The ref of the sketch block's last body item after mutation. We need to
6628    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6629    /// reference.
6630    found: Cell<Option<AstNodeRef>>,
6631}
6632
6633impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6634    type Error = crate::front::Error;
6635
6636    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6637        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6638            return Ok(true);
6639        };
6640
6641        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6642            if let Some(node_path) = node_path
6643                && node_path == self.target_node_path
6644            {
6645                self.found.set(sketch_block.body.items.last().map(|item| match item {
6646                    // For declarations like `circle1 = circle(...)`, use
6647                    // the init expression range so lookup in source_range_to_object
6648                    // matches the segment source range.
6649                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6650                    _ => AstNodeRef::from(item),
6651                }));
6652
6653                return Ok(false);
6654            } else {
6655                // We found a different sketch block. No need to descend into
6656                // its children since sketch blocks cannot be nested.
6657                return Ok(true);
6658            }
6659        }
6660
6661        for child in node.children().iter() {
6662            if !child.visit(*self)? {
6663                return Ok(false);
6664            }
6665        }
6666
6667        Ok(true)
6668    }
6669}
6670
6671/// After adding an item to a sketch block, find the sketch block, and get the
6672/// source range of the added item. We assume that the added item is the last
6673/// item in the sketch block and that the sketch block's source range has grown,
6674/// but not moved from its starting offset.
6675///
6676/// TODO: Do we need to format *before* mutation in case formatting moves the
6677/// sketch block forward?
6678fn find_sketch_block_added_item(
6679    ast: &ast::Node<ast::Program>,
6680    sketch_block_before_mutation: &AstNodeRef,
6681) -> Result<AstNodeRef, KclError> {
6682    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6683        let find = FindSketchBlockByNodePath {
6684            target_node_path: node_path.clone(),
6685            found: Cell::new(None),
6686        };
6687        let node = crate::walk::Node::from(ast);
6688        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689        find.found.into_inner().ok_or_else(|| {
6690            KclError::refactor(format!(
6691                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6692            ))
6693        })
6694    } else {
6695        // No NodePath. Fall back to legacy source range.
6696        let find = FindSketchBlockSourceRange {
6697            target_before_mutation: sketch_block_before_mutation.range,
6698            found: Cell::new(None),
6699        };
6700        let node = crate::walk::Node::from(ast);
6701        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6702        find.found.into_inner().ok_or_else(|| KclError::refactor(
6703            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?"),
6704        ))
6705    }
6706}
6707
6708fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6709    let message = error.message().trim();
6710    let message = if message.is_empty() {
6711        "unknown parse error"
6712    } else {
6713        message
6714    };
6715
6716    format!("{prefix}: {message}")
6717}
6718
6719fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6720    let (program, errors) = Program::parse(source).map_err(|err| {
6721        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6722    })?;
6723    if !errors.is_empty() {
6724        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6725            format_compilation_issues(parse_error_prefix, &errors),
6726        )));
6727    }
6728
6729    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6730}
6731
6732fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6733    let Some(first_issue) = issues
6734        .iter()
6735        .find(|issue| issue.severity.is_err())
6736        .or_else(|| issues.first())
6737    else {
6738        return prefix.to_owned();
6739    };
6740
6741    let message = first_issue.message.trim();
6742    let message = if message.is_empty() {
6743        "unknown parse error"
6744    } else {
6745        message
6746    };
6747
6748    if issues.len() > 1 {
6749        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6750    } else {
6751        format!("{prefix}: {message}")
6752    }
6753}
6754
6755fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6756    // TODO: Don't duplicate this from lib.rs Program.
6757    ast.recast_top(&Default::default(), 0)
6758}
6759
6760struct FindNumericLiteral {
6761    target: SourceRange,
6762    found: Cell<Option<ast::NumericLiteral>>,
6763}
6764
6765impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6766    type Error = crate::front::Error;
6767
6768    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6769        let Ok(node_range) = SourceRange::try_from(&node) else {
6770            return Ok(true);
6771        };
6772
6773        if node_range == self.target
6774            && let crate::walk::Node::NumericLiteral(literal) = node
6775        {
6776            self.found.set(Some(literal.inner.clone()));
6777            return Ok(false);
6778        }
6779
6780        for child in node.children().iter() {
6781            if !child.visit(*self)? {
6782                return Ok(false);
6783            }
6784        }
6785
6786        Ok(true)
6787    }
6788}
6789
6790fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6791    let find = FindNumericLiteral {
6792        target,
6793        found: Cell::new(None),
6794    };
6795    let node = crate::walk::Node::from(ast);
6796    node.visit(&find).ok()?;
6797    find.found.into_inner()
6798}
6799
6800struct FindSketchVarInitialByNodePath<'a> {
6801    target: &'a ast::NodePath,
6802    sketch_var_found: Cell<bool>,
6803    initial_literal: Cell<Option<ast::NumericLiteral>>,
6804}
6805
6806impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6807    type Error = crate::front::Error;
6808
6809    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6810        if let crate::walk::Node::SketchVar(sketch_var) = node
6811            && sketch_var.node_path.as_ref() == Some(self.target)
6812        {
6813            self.sketch_var_found.set(true);
6814            if let Some(initial) = &sketch_var.initial {
6815                self.initial_literal.set(Some(initial.inner.clone()));
6816            }
6817            return Ok(false);
6818        }
6819
6820        for child in node.children().iter() {
6821            if !child.visit(*self)? {
6822                return Ok(false);
6823            }
6824        }
6825
6826        Ok(true)
6827    }
6828}
6829
6830/// Locate the source `var` declaration corresponding to a sketch-var solution.
6831///
6832/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6833/// from "target found." The inner [`Option`] is the initial numeric literal of
6834/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6835///
6836/// When `node_path` is `None` (e.g. for older outcomes that predate the
6837/// node-path propagation), this falls back to source-range matching, which
6838/// can break under whitespace shifts elsewhere in the file.
6839fn numeric_literal_at_node_path(
6840    ast: &ast::Node<ast::Program>,
6841    node_path: Option<&ast::NodePath>,
6842    source_range: SourceRange,
6843) -> Option<Option<ast::NumericLiteral>> {
6844    let Some(node_path) = node_path else {
6845        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";
6846        #[cfg(target_arch = "wasm32")]
6847        web_sys::console::warn_1(&message.into());
6848        #[cfg(not(target_arch = "wasm32"))]
6849        eprintln!("WARNING: {message}");
6850        return numeric_literal_at_source_range(ast, source_range).map(Some);
6851    };
6852    let find = FindSketchVarInitialByNodePath {
6853        target: node_path,
6854        sketch_var_found: Cell::new(false),
6855        initial_literal: Cell::new(None),
6856    };
6857    let node = crate::walk::Node::from(ast);
6858    node.visit(&find).ok()?;
6859    if !find.sketch_var_found.get() {
6860        return None;
6861    }
6862    Some(find.initial_literal.into_inner())
6863}
6864
6865fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6866    match suffix {
6867        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6868        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6869        NumericSuffix::M => Some(UnitLength::Meters),
6870        NumericSuffix::Inch => Some(UnitLength::Inches),
6871        NumericSuffix::Ft => Some(UnitLength::Feet),
6872        NumericSuffix::Yd => Some(UnitLength::Yards),
6873        _ => None,
6874    }
6875}
6876
6877fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6878    match suffix_length_unit(number.units) {
6879        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6880        None => number.value,
6881    }
6882}
6883
6884fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6885    match suffix_length_unit(literal.suffix) {
6886        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6887        None => literal.value,
6888    }
6889}
6890
6891fn var_solution_needs_commit(
6892    current_literal: &ast::NumericLiteral,
6893    solved_value: Number,
6894    default_length_unit: UnitLength,
6895) -> bool {
6896    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6897    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6898
6899    (current - solved).abs() > 1e-9
6900}
6901
6902fn preserve_var_solution_literal_style(
6903    current_literal: &ast::NumericLiteral,
6904    solved_value: Number,
6905    default_length_unit: UnitLength,
6906) -> Number {
6907    if current_literal.suffix == NumericSuffix::None {
6908        return Number {
6909            value: number_value_in_default_length_units(solved_value, default_length_unit),
6910            units: NumericSuffix::None,
6911        };
6912    }
6913
6914    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6915        return solved_value;
6916    };
6917
6918    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6919    Number {
6920        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6921        units: current_literal.suffix,
6922    }
6923}
6924
6925pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6926    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6927        inner: ast::ArrayExpression {
6928            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6929            non_code_meta: Default::default(),
6930            digest: None,
6931        },
6932        start: Default::default(),
6933        end: Default::default(),
6934        module_id: Default::default(),
6935        node_path: None,
6936        outer_attrs: Default::default(),
6937        pre_comments: Default::default(),
6938        comment_start: Default::default(),
6939    })))
6940}
6941
6942pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6943    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6944        ast::ArrayExpression {
6945            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6946            digest: None,
6947            non_code_meta: Default::default(),
6948        },
6949    ))))
6950}
6951
6952fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6953    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6954        ast::ArrayExpression {
6955            elements: vec![
6956                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6957                    point.x,
6958                )?)))),
6959                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6960                    point.y,
6961                )?)))),
6962            ],
6963            non_code_meta: Default::default(),
6964            digest: None,
6965        },
6966    ))))
6967}
6968
6969fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6970    match expr {
6971        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6972            inner: ast::Literal::from(to_source_number(*number)?),
6973            start: Default::default(),
6974            end: Default::default(),
6975            module_id: Default::default(),
6976            node_path: None,
6977            outer_attrs: Default::default(),
6978            pre_comments: Default::default(),
6979            comment_start: Default::default(),
6980        }))),
6981        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6982            inner: ast::SketchVar {
6983                initial: Some(BoxNode::new(ast::Node {
6984                    inner: to_source_number(*number)?,
6985                    start: Default::default(),
6986                    end: Default::default(),
6987                    module_id: Default::default(),
6988                    node_path: None,
6989                    outer_attrs: Default::default(),
6990                    pre_comments: Default::default(),
6991                    comment_start: Default::default(),
6992                })),
6993                digest: None,
6994            },
6995            start: Default::default(),
6996            end: Default::default(),
6997            module_id: Default::default(),
6998            node_path: None,
6999            outer_attrs: Default::default(),
7000            pre_comments: Default::default(),
7001            comment_start: Default::default(),
7002        }))),
7003        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7004    }
7005}
7006
7007fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7008    Ok(ast::NumericLiteral {
7009        value: number.value,
7010        suffix: number.units,
7011        raw: format_number_literal(number.value, number.units, None)?,
7012        digest: None,
7013    })
7014}
7015
7016pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7017    ast::Expr::Name(BoxNode::new(ast_name(name)))
7018}
7019
7020fn ast_name(name: String) -> ast::Node<ast::Name> {
7021    ast::Node {
7022        inner: ast::Name {
7023            name: ast::Node {
7024                inner: ast::Identifier { name, digest: None },
7025                start: Default::default(),
7026                end: Default::default(),
7027                module_id: Default::default(),
7028                node_path: None,
7029                outer_attrs: Default::default(),
7030                pre_comments: Default::default(),
7031                comment_start: Default::default(),
7032            },
7033            path: Vec::new(),
7034            abs_path: false,
7035            digest: None,
7036        },
7037        start: Default::default(),
7038        end: Default::default(),
7039        module_id: Default::default(),
7040        node_path: None,
7041        outer_attrs: Default::default(),
7042        pre_comments: Default::default(),
7043        comment_start: Default::default(),
7044    }
7045}
7046
7047pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7048    ast::Name {
7049        name: ast::Node {
7050            inner: ast::Identifier {
7051                name: name.to_owned(),
7052                digest: None,
7053            },
7054            start: Default::default(),
7055            end: Default::default(),
7056            module_id: Default::default(),
7057            node_path: None,
7058            outer_attrs: Default::default(),
7059            pre_comments: Default::default(),
7060            comment_start: Default::default(),
7061        },
7062        path: Default::default(),
7063        abs_path: false,
7064        digest: None,
7065    }
7066}
7067
7068/// Create an AST node for coincident([expr1, expr2, ...])
7069pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7070    let elements = exprs.into_iter().collect::<Vec<_>>();
7071    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7072
7073    // Create array [expr1, expr2, ...]
7074    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7075        elements,
7076        digest: None,
7077        non_code_meta: Default::default(),
7078    })));
7079
7080    // Create coincident([...])
7081    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7082        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7083        unlabeled: Some(array_expr),
7084        arguments: Default::default(),
7085        digest: None,
7086        non_code_meta: Default::default(),
7087    })))
7088}
7089
7090/// Create an AST node for horizontal(line)
7091pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7092    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7093        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7094        unlabeled: Some(line_expr),
7095        arguments: Default::default(),
7096        digest: None,
7097        non_code_meta: Default::default(),
7098    })))
7099}
7100
7101/// Create an AST node for vertical(line)
7102pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7103    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7104        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7105        unlabeled: Some(line_expr),
7106        arguments: Default::default(),
7107        digest: None,
7108        non_code_meta: Default::default(),
7109    })))
7110}
7111
7112/// Create a member expression like object.property (e.g., line1.end)
7113pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7114    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7115        object: object_expr,
7116        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7117            name: ast::Node::no_src(ast::Identifier {
7118                name: property.to_string(),
7119                digest: None,
7120            }),
7121            path: Vec::new(),
7122            abs_path: false,
7123            digest: None,
7124        }))),
7125        computed: false,
7126        digest: None,
7127    })))
7128}
7129
7130pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7131    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7132        object: object_expr,
7133        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7134            ast::NumericLiteral {
7135                value: index as f64,
7136                suffix: NumericSuffix::None,
7137                raw: index.to_string(),
7138                digest: None,
7139            },
7140        )))),
7141        computed: true,
7142        digest: None,
7143    })))
7144}
7145
7146/// Create an AST node for `fixed([point, [x, y]])`.
7147fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7148    // Create [x, y] array literal.
7149    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7150        position.x,
7151    )?))));
7152    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7153        position.y,
7154    )?))));
7155    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7156        elements: vec![x_literal, y_literal],
7157        digest: None,
7158        non_code_meta: Default::default(),
7159    })));
7160
7161    // Create [point, [x, y]] outer array.
7162    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7163        elements: vec![point_expr, point_array],
7164        digest: None,
7165        non_code_meta: Default::default(),
7166    })));
7167
7168    // Create fixed([...])
7169    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7170        ast::CallExpressionKw {
7171            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7172            unlabeled: Some(array_expr),
7173            arguments: Default::default(),
7174            digest: None,
7175            non_code_meta: Default::default(),
7176        },
7177    ))))
7178}
7179
7180/// Create an AST node for equalLength([line1, line2, ...])
7181pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7182    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7183        elements: line_exprs,
7184        digest: None,
7185        non_code_meta: Default::default(),
7186    })));
7187
7188    // Create equalLength([...])
7189    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7190        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7191        unlabeled: Some(array_expr),
7192        arguments: Default::default(),
7193        digest: None,
7194        non_code_meta: Default::default(),
7195    })))
7196}
7197
7198/// Create an AST node for equalRadius([seg1, seg2, ...])
7199pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7200    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7201        elements: segment_exprs,
7202        digest: None,
7203        non_code_meta: Default::default(),
7204    })));
7205
7206    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7207        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7208        unlabeled: Some(array_expr),
7209        arguments: Default::default(),
7210        digest: None,
7211        non_code_meta: Default::default(),
7212    })))
7213}
7214
7215/// Create an AST node for tangent([seg1, seg2])
7216pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7217    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7218        elements: vec![seg1_expr, seg2_expr],
7219        digest: None,
7220        non_code_meta: Default::default(),
7221    })));
7222
7223    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7224        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7225        unlabeled: Some(array_expr),
7226        arguments: Default::default(),
7227        digest: None,
7228        non_code_meta: Default::default(),
7229    })))
7230}
7231
7232/// Create an AST node for symmetric([input1, input2], axis = line)
7233pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7234    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7235        elements: input_exprs,
7236        digest: None,
7237        non_code_meta: Default::default(),
7238    })));
7239    let arguments = vec![ast::LabeledArg {
7240        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7241        arg: axis_expr,
7242    }];
7243
7244    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7245        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7246        unlabeled: Some(array_expr),
7247        arguments,
7248        digest: None,
7249        non_code_meta: Default::default(),
7250    })))
7251}
7252
7253/// Create an AST node for midpoint(segment, point = point)
7254pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7255    let arguments = vec![ast::LabeledArg {
7256        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7257        arg: point_expr,
7258    }];
7259
7260    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7261        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7262        unlabeled: Some(segment_expr),
7263        arguments,
7264        digest: None,
7265        non_code_meta: Default::default(),
7266    })))
7267}
7268
7269/// The source range to attach to a diagnostic for `error` in the top-level
7270/// module. Source ranges are ordered innermost first and may point into
7271/// imported modules, so prefer the innermost range that is in the top-level
7272/// module; otherwise fall back to the innermost range.
7273fn issue_source_range(error: &KclError) -> SourceRange {
7274    let source_ranges = error.source_ranges();
7275    source_ranges
7276        .iter()
7277        .find(|range| range.is_top_level_module())
7278        .or_else(|| source_ranges.first())
7279        .copied()
7280        .unwrap_or_else(SourceRange::synthetic)
7281}
7282
7283#[cfg(test)]
7284mod tests {
7285    use std::sync;
7286
7287    use kcl_api::Distance;
7288    use kcl_api::Fixed;
7289    use kcl_api::FixedPoint;
7290    use kcl_api::Midpoint;
7291    use kcl_api::NumericSuffix;
7292    use kcl_api::Object;
7293    use kcl_api::Plane;
7294    use kcl_api::Sketch;
7295    use kcl_api::Tangent;
7296    use kcl_api::Vertical;
7297
7298    use super::*;
7299    use crate::engine::PlaneName;
7300    use crate::engine::engine_manager::EngineManager;
7301    use crate::execution::cache::SketchModeState;
7302
7303    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7304        for object in &scene_graph.objects {
7305            if let ObjectKind::Sketch(_) = &object.kind {
7306                return Some(object);
7307            }
7308        }
7309        None
7310    }
7311
7312    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7313        for object in &scene_graph.objects {
7314            if let ObjectKind::Face(_) = &object.kind {
7315                return Some(object);
7316            }
7317        }
7318        None
7319    }
7320
7321    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7322        for object in &scene_graph.objects {
7323            if matches!(&object.kind, ObjectKind::Wall(_)) {
7324                return Some(object.id);
7325            }
7326        }
7327        None
7328    }
7329
7330    fn find_cap_object_id_with_solid_output_index(
7331        scene_graph: &SceneGraph,
7332        cap_kind: kcl_api::CapKind,
7333        solid_output_index: usize,
7334    ) -> Option<ObjectId> {
7335        for object in &scene_graph.objects {
7336            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7337            {
7338                return Some(object.id);
7339            }
7340        }
7341        None
7342    }
7343
7344    #[test]
7345    fn issue_source_range_prefers_top_level_module() {
7346        use kcl_error::ModuleId;
7347
7348        let top = SourceRange::new(10, 20, ModuleId::default());
7349        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7350
7351        // Innermost frame is in an imported module; the diagnostic should
7352        // land on the innermost top-level range instead.
7353        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7354            "boom".to_owned(),
7355            vec![imported, top],
7356        ));
7357        assert_eq!(super::issue_source_range(&error), top);
7358
7359        // No top-level range at all: fall back to the innermost one.
7360        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7361        assert_eq!(super::issue_source_range(&error), imported);
7362
7363        // No ranges: synthetic.
7364        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7365        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7366    }
7367
7368    #[test]
7369    fn composite_constituent_sweeps_are_not_solid_outputs() {
7370        use kcl_api::artifact::ArtifactSweepMethod;
7371        use kcl_api::artifact::CompositeSolid;
7372        use kcl_api::artifact::CompositeSolidSubType;
7373        use kcl_api::artifact::Sweep;
7374        use kcl_api::artifact::SweepSubType;
7375
7376        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7377        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7378        let composite_id = ArtifactId::new(Uuid::new_v4());
7379        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7380        let sweep = |id| {
7381            Artifact::Sweep(Sweep {
7382                id,
7383                sub_type: SweepSubType::Extrusion,
7384                path_id: Some(ArtifactId::new(Uuid::new_v4())),
7385                surface_ids: Vec::new(),
7386                edge_ids: Vec::new(),
7387                code_ref: code_ref.clone(),
7388                source_sweep_id: None,
7389                trajectory_id: None,
7390                method: ArtifactSweepMethod::New,
7391                consumed: false,
7392                pattern_ids: Vec::new(),
7393            })
7394        };
7395        let mut artifacts = IndexMap::from([
7396            (first_sweep_id, sweep(first_sweep_id)),
7397            (second_sweep_id, sweep(second_sweep_id)),
7398        ]);
7399
7400        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7401        assert_eq!(
7402            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7403            Some(0)
7404        );
7405        assert_eq!(
7406            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7407            Some(1)
7408        );
7409
7410        artifacts.insert(
7411            composite_id,
7412            Artifact::CompositeSolid(CompositeSolid {
7413                id: composite_id,
7414                consumed: false,
7415                sub_type: CompositeSolidSubType::Union,
7416                output_index: None,
7417                solid_ids: vec![first_sweep_id, second_sweep_id],
7418                tool_ids: Vec::new(),
7419                code_ref,
7420                composite_solid_id: None,
7421                pattern_ids: Vec::new(),
7422            }),
7423        );
7424        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7425        assert_eq!(
7426            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7427            None
7428        );
7429        assert_eq!(
7430            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7431            None
7432        );
7433    }
7434
7435    #[test]
7436    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7437        let source = "\
7438region001 = region(point = [0.1, 0.1], sketch = s)
7439extrude001 = extrude(region001, length = 5)
7440revolve001 = revolve(region001, axis = Y)
7441sweep001 = sweep(region001, path = path001)
7442loft001 = loft(region001)
7443not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7444";
7445
7446        let program = Program::parse(source).unwrap().0.unwrap();
7447
7448        assert_eq!(
7449            region_name_from_sweep_variable(&program.ast, "extrude001"),
7450            Some("region001".to_owned())
7451        );
7452        assert_eq!(
7453            region_name_from_sweep_variable(&program.ast, "revolve001"),
7454            Some("region001".to_owned())
7455        );
7456        assert_eq!(
7457            region_name_from_sweep_variable(&program.ast, "sweep001"),
7458            Some("region001".to_owned())
7459        );
7460        assert_eq!(
7461            region_name_from_sweep_variable(&program.ast, "loft001"),
7462            Some("region001".to_owned())
7463        );
7464        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7465    }
7466
7467    #[track_caller]
7468    fn expect_sketch(object: &Object) -> &Sketch {
7469        if let ObjectKind::Sketch(sketch) = &object.kind {
7470            sketch
7471        } else {
7472            panic!("Object is not a sketch: {:?}", object);
7473        }
7474    }
7475
7476    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7477        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7478        let ObjectKind::Segment {
7479            segment: Segment::Point(point),
7480        } = &point_object.kind
7481        else {
7482            panic!("Object is not a point segment: {point_object:?}");
7483        };
7484        point.position.clone()
7485    }
7486
7487    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7488        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7489        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7490    }
7491
7492    /// Build a millimeter-valued point expression for concise sketch edit test
7493    /// setup.
7494    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7495        Point2d {
7496            x: Expr::Var(Number {
7497                value: x,
7498                units: NumericSuffix::Mm,
7499            }),
7500            y: Expr::Var(Number {
7501                value: y,
7502                units: NumericSuffix::Mm,
7503            }),
7504        }
7505    }
7506
7507    /// Build a millimeter-valued numeric point for comparing solved scene graph
7508    /// positions.
7509    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7510        Point2d {
7511            x: Number {
7512                value: x,
7513                units: NumericSuffix::Mm,
7514            },
7515            y: Number {
7516                value: y,
7517                units: NumericSuffix::Mm,
7518            },
7519        }
7520    }
7521
7522    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7523        LineCtor {
7524            start: Point2d {
7525                x: Expr::Number(Number { value: start_x, units }),
7526                y: Expr::Number(Number { value: start_y, units }),
7527            },
7528            end: Point2d {
7529                x: Expr::Number(Number { value: end_x, units }),
7530                y: Expr::Number(Number { value: end_y, units }),
7531            },
7532            construction: None,
7533        }
7534    }
7535
7536    async fn create_sketch_with_single_line(
7537        frontend: &mut FrontendState,
7538        ctx: &ExecutorContext,
7539        mock_ctx: &ExecutorContext,
7540        version: Version,
7541    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7542        frontend.program = Program::empty();
7543
7544        let sketch_args = SketchCtor {
7545            on: Plane::Default(PlaneName::Xy),
7546        };
7547        let (_src_delta, _scene_delta, sketch_id) = frontend
7548            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7549            .await
7550            .unwrap();
7551
7552        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7553        let (source_delta, scene_graph_delta) = frontend
7554            .add_segment(mock_ctx, version, sketch_id, segment, None)
7555            .await
7556            .unwrap();
7557        let line_id = *scene_graph_delta
7558            .new_objects
7559            .last()
7560            .expect("Expected line object id to be created");
7561
7562        (sketch_id, line_id, source_delta, scene_graph_delta)
7563    }
7564
7565    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7566        frontend.program = program.clone();
7567        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7568        frontend.update_state_after_exec(outcome, true);
7569    }
7570
7571    #[test]
7572    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7573        for (source, expected_message) in [
7574            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7575            (
7576                "3'",
7577                "Error parsing KCL source after editing: unterminated string literal",
7578            ),
7579        ] {
7580            let err = parse_frontend_mutation_source(
7581                source,
7582                "Error parsing KCL source after editing",
7583                "No AST produced after editing",
7584            )
7585            .expect_err("expected invalid KCL source to fail");
7586            let message = err.error.message();
7587
7588            assert_eq!(message, expected_message);
7589            assert!(!message.contains("CompilationIssue"));
7590            assert!(!message.contains("KclErrorDetails"));
7591            assert!(!message.contains("source_range"));
7592        }
7593    }
7594
7595    #[tokio::test(flavor = "multi_thread")]
7596    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7597        let initial_source = "\
7598sketch(on = XY) {
7599  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7600  distance([line1.start, line1.end]) == 10
7601}
7602";
7603        let program = Program::parse(initial_source).unwrap().0.unwrap();
7604
7605        let mut frontend = FrontendState::new();
7606        let mock_ctx = ExecutorContext::new_mock(None).await;
7607        let version = Version(0);
7608
7609        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7610        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7611        let sketch_id = sketch_object.id;
7612        let sketch = expect_sketch(sketch_object);
7613        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7614
7615        for (value, expected_message) in [
7616            ("**", "Invalid constraint value: Unexpected token: *"),
7617            ("3'", "Invalid constraint value: unterminated string literal"),
7618        ] {
7619            let err = frontend
7620                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7621                .await
7622                .expect_err("expected invalid constraint expression to fail");
7623            let message = err.error.message();
7624
7625            assert_eq!(message, expected_message);
7626            assert!(!message.contains("CompilationIssue"));
7627            assert!(!message.contains("KclErrorDetails"));
7628            assert!(!message.contains("source_range"));
7629        }
7630
7631        mock_ctx.close().await;
7632    }
7633
7634    #[tokio::test(flavor = "multi_thread")]
7635    async fn test_failed_edit_constraint_value_does_not_update_program() {
7636        let initial_source = "\
7637sketch(on = XY) {
7638  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7639  distance([line1.start, line1.end]) == 10
7640}
7641";
7642        let program = Program::parse(initial_source).unwrap().0.unwrap();
7643        let original_source = program.original_file_contents.clone();
7644
7645        let mut frontend = FrontendState::new();
7646        let mock_ctx = ExecutorContext::new_mock(None).await;
7647        let version = Version(0);
7648
7649        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7650        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7651        let sketch_id = sketch_object.id;
7652        let sketch = expect_sketch(sketch_object);
7653        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7654
7655        frontend
7656            .edit_constraint_value(
7657                &mock_ctx,
7658                version,
7659                sketch_id,
7660                constraint_id,
7661                "unknownDistance".to_owned(),
7662            )
7663            .await
7664            .expect_err("expected invalid constraint value to fail execution");
7665
7666        assert_eq!(frontend.program.original_file_contents, original_source);
7667        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7668
7669        mock_ctx.close().await;
7670    }
7671
7672    #[tokio::test(flavor = "multi_thread")]
7673    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7674        let initial_source = "\
7675arr = [0]
7676sketch(on = XY) {
7677  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7678  distance([line1.start, line1.end]) == 10
7679}
7680";
7681        let program = Program::parse(initial_source).unwrap().0.unwrap();
7682
7683        let mut frontend = FrontendState::new();
7684        let mock_ctx = ExecutorContext::new_mock(None).await;
7685        let version = Version(0);
7686
7687        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7688        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7689        let sketch_id = sketch_object.id;
7690        let sketch = expect_sketch(sketch_object);
7691        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7692
7693        // In sketch mode execution, an out-of-bounds array index is an error.
7694        // It should not fall back to the first element of the array the way
7695        // plain mock execution does.
7696        let err = frontend
7697            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7698            .await
7699            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7700        let message = err.error.message();
7701        assert!(
7702            message.contains("The array doesn't have any item at index 5"),
7703            "unexpected error message: {message}"
7704        );
7705
7706        mock_ctx.close().await;
7707    }
7708
7709    #[tokio::test(flavor = "multi_thread")]
7710    async fn test_sketch_checkpoint_round_trip_restores_state() {
7711        let mut frontend = FrontendState::new();
7712        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7713        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7714        let version = Version(0);
7715
7716        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7717            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7718
7719        let expected_source = source_delta.text.clone();
7720        let expected_scene_graph = frontend.scene_graph.clone();
7721        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7722        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7723
7724        let checkpoint_id = frontend
7725            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7726            .await
7727            .unwrap();
7728
7729        let edited_segments = vec![ExistingSegmentCtor {
7730            id: line_id,
7731            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7732        }];
7733        let (edited_source, _edited_scene) = frontend
7734            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7735            .await
7736            .unwrap();
7737        assert_ne!(edited_source.text, expected_source);
7738
7739        let restored = frontend
7740            .restore_sketch_checkpoint(&ctx.cache, checkpoint_id)
7741            .await
7742            .unwrap();
7743
7744        assert_eq!(restored.source_delta.text, expected_source);
7745        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7746        assert!(restored.scene_graph_delta.invalidates_ids);
7747        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7748        assert_eq!(frontend.scene_graph, expected_scene_graph);
7749        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7750
7751        ctx.close().await;
7752    }
7753
7754    #[tokio::test(flavor = "multi_thread")]
7755    async fn test_sketch_checkpoints_prune_oldest_entries() {
7756        let mut frontend = FrontendState::new();
7757        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7758        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7759        let version = Version(0);
7760
7761        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7762            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7763
7764        let mut checkpoint_ids = Vec::new();
7765        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7766            checkpoint_ids.push(
7767                frontend
7768                    .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7769                    .await
7770                    .unwrap(),
7771            );
7772        }
7773
7774        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7775        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7776
7777        let oldest_retained = checkpoint_ids[3];
7778        assert_eq!(
7779            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7780            Some(oldest_retained)
7781        );
7782
7783        let evicted_restore = frontend.restore_sketch_checkpoint(&ctx.cache, checkpoint_ids[0]).await;
7784        assert!(evicted_restore.is_err());
7785        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7786
7787        frontend
7788            .restore_sketch_checkpoint(&ctx.cache, *checkpoint_ids.last().unwrap())
7789            .await
7790            .unwrap();
7791
7792        ctx.close().await;
7793    }
7794
7795    #[tokio::test(flavor = "multi_thread")]
7796    async fn test_sketch_checkpoint_uses_explicit_session_cache() {
7797        let mut frontend = FrontendState::new();
7798        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
7799        let other = ExecutorContext::new_mock(None).await;
7800        let program = Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
7801        let outcome = ctx.run_with_caching(program).await.unwrap();
7802        let checkpoint = frontend.create_sketch_checkpoint(&ctx.cache, outcome).await.unwrap();
7803        ctx.cache.clear_mem_cache().await;
7804        assert!(ctx.cache.read_old_memory_var("x").await.is_none());
7805        frontend
7806            .restore_sketch_checkpoint(&ctx.cache, checkpoint)
7807            .await
7808            .unwrap();
7809        assert!(ctx.cache.read_old_memory_var("x").await.is_some());
7810        assert!(other.cache.read_old_memory_var("x").await.is_none());
7811        ctx.close().await;
7812        other.close().await;
7813    }
7814
7815    #[tokio::test(flavor = "multi_thread")]
7816    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7817        let mut frontend = FrontendState::new();
7818        let ctx = ExecutorContext::new_mock(None).await;
7819        let missing_checkpoint = SketchCheckpointId::new(999);
7820
7821        let err = frontend
7822            .restore_sketch_checkpoint(&ctx.cache, missing_checkpoint)
7823            .await
7824            .expect_err("Expected restore to fail for missing checkpoint");
7825
7826        assert!(err.msg.contains("Sketch checkpoint not found"));
7827    }
7828
7829    #[tokio::test(flavor = "multi_thread")]
7830    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7831        let mut frontend = FrontendState::new();
7832        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7833        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7834        let version = Version(0);
7835
7836        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7837            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7838
7839        let checkpoint_a = frontend
7840            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7841            .await
7842            .unwrap();
7843        let checkpoint_b = frontend
7844            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7845            .await
7846            .unwrap();
7847        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7848
7849        frontend.clear_sketch_checkpoints();
7850        assert!(frontend.sketch_checkpoints.is_empty());
7851        frontend
7852            .restore_sketch_checkpoint(&ctx.cache, checkpoint_a)
7853            .await
7854            .unwrap_err();
7855        frontend
7856            .restore_sketch_checkpoint(&ctx.cache, checkpoint_b)
7857            .await
7858            .unwrap_err();
7859
7860        ctx.close().await;
7861    }
7862
7863    #[tokio::test(flavor = "multi_thread")]
7864    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7865        let mut frontend = FrontendState::new();
7866        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7867        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7868        let version = Version(0);
7869
7870        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7871            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7872        let old_source = source_delta.text.clone();
7873        let old_checkpoint = frontend
7874            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7875            .await
7876            .unwrap();
7877        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7878
7879        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7880            .unwrap()
7881            .0
7882            .unwrap();
7883
7884        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7885        let SetProgramOutcome::Success {
7886            checkpoint_id: Some(new_checkpoint),
7887            ..
7888        } = result
7889        else {
7890            panic!("Expected Success with a fresh checkpoint baseline");
7891        };
7892
7893        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7894
7895        let old_restore = frontend
7896            .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7897            .await
7898            .unwrap();
7899        assert_eq!(old_restore.source_delta.text, old_source);
7900
7901        let new_restore = frontend
7902            .restore_sketch_checkpoint(&ctx.cache, new_checkpoint)
7903            .await
7904            .unwrap();
7905        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7906
7907        ctx.close().await;
7908    }
7909
7910    #[tokio::test(flavor = "multi_thread")]
7911    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7912        let mut frontend = FrontendState::new();
7913        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7914        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7915        let version = Version(0);
7916
7917        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7918            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7919        let old_checkpoint = frontend
7920            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7921            .await
7922            .unwrap();
7923        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7924
7925        let failing_program = Program::parse(
7926            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7927        )
7928        .unwrap()
7929        .0
7930        .unwrap();
7931
7932        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7933        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7934        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7935        frontend
7936            .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7937            .await
7938            .unwrap();
7939
7940        ctx.close().await;
7941    }
7942
7943    #[tokio::test(flavor = "multi_thread")]
7944    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7945        let mut frontend = FrontendState::new();
7946        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7947
7948        let program = Program::parse(
7949            "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",
7950        )
7951        .unwrap()
7952        .0
7953        .unwrap();
7954        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7955        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7956            panic!("Expected successful baseline program execution");
7957        };
7958
7959        ctx.cache.clear_mem_cache().await;
7960        assert!(ctx.cache.read_old_memory().await.is_none());
7961
7962        let checkpoint_without_mock_memory = frontend
7963            .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7964            .await
7965            .unwrap();
7966
7967        ctx.cache.write_old_memory(SketchModeState::new_for_tests()).await;
7968        assert!(ctx.cache.read_old_memory().await.is_some());
7969
7970        let checkpoint_with_mock_memory = frontend
7971            .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7972            .await
7973            .unwrap();
7974
7975        ctx.cache.clear_mem_cache().await;
7976        assert!(ctx.cache.read_old_memory().await.is_none());
7977
7978        frontend
7979            .restore_sketch_checkpoint(&ctx.cache, checkpoint_with_mock_memory)
7980            .await
7981            .unwrap();
7982        assert!(ctx.cache.read_old_memory().await.is_some());
7983
7984        frontend
7985            .restore_sketch_checkpoint(&ctx.cache, checkpoint_without_mock_memory)
7986            .await
7987            .unwrap();
7988        assert!(ctx.cache.read_old_memory().await.is_none());
7989
7990        ctx.close().await;
7991    }
7992
7993    #[tokio::test(flavor = "multi_thread")]
7994    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7995        let source = "\
7996sketch(on = XY) {
7997  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7998}
7999
8000bad = missing_name
8001";
8002        let program = Program::parse(source).unwrap().0.unwrap();
8003
8004        let mut frontend = FrontendState::new();
8005
8006        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8007        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8008        let version = Version(0);
8009        let project_id = ProjectId(0);
8010        let file_id = FileId(0);
8011
8012        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8013            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8014        };
8015
8016        let sketch_id = frontend
8017            .scene_graph
8018            .objects
8019            .iter()
8020            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8021            .expect("Expected sketch object from errored hack_set_program");
8022
8023        frontend
8024            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8025            .await
8026            .unwrap();
8027
8028        ctx.close().await;
8029        mock_ctx.close().await;
8030    }
8031
8032    #[tokio::test(flavor = "multi_thread")]
8033    async fn test_new_sketch_add_point_edit_point() {
8034        let program = Program::empty();
8035
8036        let mut frontend = FrontendState::new();
8037        frontend.program = program;
8038
8039        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8040        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8041        let version = Version(0);
8042
8043        let sketch_args = SketchCtor {
8044            on: Plane::Default(PlaneName::Xy),
8045        };
8046        let (_src_delta, scene_delta, sketch_id) = frontend
8047            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8048            .await
8049            .unwrap();
8050        assert_eq!(sketch_id, ObjectId(1));
8051        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8052        let sketch_object = &scene_delta.new_graph.objects[1];
8053        assert_eq!(sketch_object.id, ObjectId(1));
8054        assert_eq!(
8055            sketch_object.kind,
8056            ObjectKind::Sketch(Sketch {
8057                args: SketchCtor {
8058                    on: Plane::Default(PlaneName::Xy)
8059                },
8060                plane: ObjectId(0),
8061                segments: vec![],
8062                constraints: vec![],
8063            })
8064        );
8065        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8066
8067        let point_ctor = PointCtor {
8068            position: Point2d {
8069                x: Expr::Number(Number {
8070                    value: 1.0,
8071                    units: NumericSuffix::Inch,
8072                }),
8073                y: Expr::Number(Number {
8074                    value: 2.0,
8075                    units: NumericSuffix::Inch,
8076                }),
8077            },
8078        };
8079        let segment = SegmentCtor::Point(point_ctor);
8080        let (src_delta, scene_delta) = frontend
8081            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8082            .await
8083            .unwrap();
8084        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8085        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8086        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8087        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8088            assert_eq!(scene_object.id.0, i);
8089        }
8090
8091        let point_id = *scene_delta.new_objects.last().unwrap();
8092
8093        let point_ctor = PointCtor {
8094            position: Point2d {
8095                x: Expr::Number(Number {
8096                    value: 3.0,
8097                    units: NumericSuffix::Inch,
8098                }),
8099                y: Expr::Number(Number {
8100                    value: 4.0,
8101                    units: NumericSuffix::Inch,
8102                }),
8103            },
8104        };
8105        let segments = vec![ExistingSegmentCtor {
8106            id: point_id,
8107            ctor: SegmentCtor::Point(point_ctor),
8108        }];
8109        let (src_delta, scene_delta) = frontend
8110            .edit_segments(&mock_ctx, version, sketch_id, segments)
8111            .await
8112            .unwrap();
8113        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8114        assert_eq!(scene_delta.new_objects, vec![]);
8115        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8116
8117        ctx.close().await;
8118        mock_ctx.close().await;
8119    }
8120
8121    #[tokio::test(flavor = "multi_thread")]
8122    async fn test_new_sketch_add_line_edit_line() {
8123        let program = Program::empty();
8124
8125        let mut frontend = FrontendState::new();
8126        frontend.program = program;
8127
8128        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8129        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8130        let version = Version(0);
8131
8132        let sketch_args = SketchCtor {
8133            on: Plane::Default(PlaneName::Xy),
8134        };
8135        let (_src_delta, scene_delta, sketch_id) = frontend
8136            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8137            .await
8138            .unwrap();
8139        assert_eq!(sketch_id, ObjectId(1));
8140        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8141        let sketch_object = &scene_delta.new_graph.objects[1];
8142        assert_eq!(sketch_object.id, ObjectId(1));
8143        assert_eq!(
8144            sketch_object.kind,
8145            ObjectKind::Sketch(Sketch {
8146                args: SketchCtor {
8147                    on: Plane::Default(PlaneName::Xy)
8148                },
8149                plane: ObjectId(0),
8150                segments: vec![],
8151                constraints: vec![],
8152            })
8153        );
8154        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8155
8156        let line_ctor = LineCtor {
8157            start: Point2d {
8158                x: Expr::Number(Number {
8159                    value: 0.0,
8160                    units: NumericSuffix::Mm,
8161                }),
8162                y: Expr::Number(Number {
8163                    value: 0.0,
8164                    units: NumericSuffix::Mm,
8165                }),
8166            },
8167            end: Point2d {
8168                x: Expr::Number(Number {
8169                    value: 10.0,
8170                    units: NumericSuffix::Mm,
8171                }),
8172                y: Expr::Number(Number {
8173                    value: 10.0,
8174                    units: NumericSuffix::Mm,
8175                }),
8176            },
8177            construction: None,
8178        };
8179        let segment = SegmentCtor::Line(line_ctor);
8180        let (src_delta, scene_delta) = frontend
8181            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8182            .await
8183            .unwrap();
8184        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8185        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8186        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8187        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8188            assert_eq!(scene_object.id.0, i);
8189        }
8190
8191        // The new objects are the end points and then the line.
8192        let line = *scene_delta.new_objects.last().unwrap();
8193
8194        let line_ctor = LineCtor {
8195            start: Point2d {
8196                x: Expr::Number(Number {
8197                    value: 1.0,
8198                    units: NumericSuffix::Mm,
8199                }),
8200                y: Expr::Number(Number {
8201                    value: 2.0,
8202                    units: NumericSuffix::Mm,
8203                }),
8204            },
8205            end: Point2d {
8206                x: Expr::Number(Number {
8207                    value: 13.0,
8208                    units: NumericSuffix::Mm,
8209                }),
8210                y: Expr::Number(Number {
8211                    value: 14.0,
8212                    units: NumericSuffix::Mm,
8213                }),
8214            },
8215            construction: None,
8216        };
8217        let segments = vec![ExistingSegmentCtor {
8218            id: line,
8219            ctor: SegmentCtor::Line(line_ctor),
8220        }];
8221        let (src_delta, scene_delta) = frontend
8222            .edit_segments(&mock_ctx, version, sketch_id, segments)
8223            .await
8224            .unwrap();
8225        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8226        assert_eq!(scene_delta.new_objects, vec![]);
8227        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8228
8229        ctx.close().await;
8230        mock_ctx.close().await;
8231    }
8232
8233    #[tokio::test(flavor = "multi_thread")]
8234    async fn test_new_sketch_add_arc_edit_arc() {
8235        let program = Program::empty();
8236
8237        let mut frontend = FrontendState::new();
8238        frontend.program = program;
8239
8240        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8241        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8242        let version = Version(0);
8243
8244        let sketch_args = SketchCtor {
8245            on: Plane::Default(PlaneName::Xy),
8246        };
8247        let (_src_delta, scene_delta, sketch_id) = frontend
8248            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8249            .await
8250            .unwrap();
8251        assert_eq!(sketch_id, ObjectId(1));
8252        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8253        let sketch_object = &scene_delta.new_graph.objects[1];
8254        assert_eq!(sketch_object.id, ObjectId(1));
8255        assert_eq!(
8256            sketch_object.kind,
8257            ObjectKind::Sketch(Sketch {
8258                args: SketchCtor {
8259                    on: Plane::Default(PlaneName::Xy),
8260                },
8261                plane: ObjectId(0),
8262                segments: vec![],
8263                constraints: vec![],
8264            })
8265        );
8266        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8267
8268        let arc_ctor = ArcCtor {
8269            start: Point2d {
8270                x: Expr::Var(Number {
8271                    value: 0.0,
8272                    units: NumericSuffix::Mm,
8273                }),
8274                y: Expr::Var(Number {
8275                    value: 0.0,
8276                    units: NumericSuffix::Mm,
8277                }),
8278            },
8279            end: Point2d {
8280                x: Expr::Var(Number {
8281                    value: 10.0,
8282                    units: NumericSuffix::Mm,
8283                }),
8284                y: Expr::Var(Number {
8285                    value: 10.0,
8286                    units: NumericSuffix::Mm,
8287                }),
8288            },
8289            center: Point2d {
8290                x: Expr::Var(Number {
8291                    value: 10.0,
8292                    units: NumericSuffix::Mm,
8293                }),
8294                y: Expr::Var(Number {
8295                    value: 0.0,
8296                    units: NumericSuffix::Mm,
8297                }),
8298            },
8299            direction: None,
8300            construction: None,
8301        };
8302        let segment = SegmentCtor::Arc(arc_ctor);
8303        let (src_delta, scene_delta) = frontend
8304            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8305            .await
8306            .unwrap();
8307        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8308        assert_eq!(
8309            scene_delta.new_objects,
8310            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8311        );
8312        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8313            assert_eq!(scene_object.id.0, i);
8314        }
8315        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8316
8317        // The new objects are the end points, the center, and then the arc.
8318        let arc = *scene_delta.new_objects.last().unwrap();
8319
8320        let arc_ctor = ArcCtor {
8321            start: Point2d {
8322                x: Expr::Var(Number {
8323                    value: 1.0,
8324                    units: NumericSuffix::Mm,
8325                }),
8326                y: Expr::Var(Number {
8327                    value: 2.0,
8328                    units: NumericSuffix::Mm,
8329                }),
8330            },
8331            end: Point2d {
8332                x: Expr::Var(Number {
8333                    value: 13.0,
8334                    units: NumericSuffix::Mm,
8335                }),
8336                y: Expr::Var(Number {
8337                    value: 14.0,
8338                    units: NumericSuffix::Mm,
8339                }),
8340            },
8341            center: Point2d {
8342                x: Expr::Var(Number {
8343                    value: 13.0,
8344                    units: NumericSuffix::Mm,
8345                }),
8346                y: Expr::Var(Number {
8347                    value: 2.0,
8348                    units: NumericSuffix::Mm,
8349                }),
8350            },
8351            direction: None,
8352            construction: None,
8353        };
8354        let segments = vec![ExistingSegmentCtor {
8355            id: arc,
8356            ctor: SegmentCtor::Arc(arc_ctor),
8357        }];
8358        let (src_delta, scene_delta) = frontend
8359            .edit_segments(&mock_ctx, version, sketch_id, segments)
8360            .await
8361            .unwrap();
8362        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8363        assert_eq!(scene_delta.new_objects, vec![]);
8364        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8365
8366        ctx.close().await;
8367        mock_ctx.close().await;
8368    }
8369
8370    #[tokio::test(flavor = "multi_thread")]
8371    async fn test_new_sketch_add_circle_edit_circle() {
8372        let program = Program::empty();
8373
8374        let mut frontend = FrontendState::new();
8375        frontend.program = program;
8376
8377        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8378        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8379        let version = Version(0);
8380
8381        let sketch_args = SketchCtor {
8382            on: Plane::Default(PlaneName::Xy),
8383        };
8384        let (_src_delta, _scene_delta, sketch_id) = frontend
8385            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8386            .await
8387            .unwrap();
8388
8389        // Add a circle segment.
8390        let circle_ctor = CircleCtor {
8391            start: Point2d {
8392                x: Expr::Var(Number {
8393                    value: 5.0,
8394                    units: NumericSuffix::Mm,
8395                }),
8396                y: Expr::Var(Number {
8397                    value: 0.0,
8398                    units: NumericSuffix::Mm,
8399                }),
8400            },
8401            center: Point2d {
8402                x: Expr::Var(Number {
8403                    value: 0.0,
8404                    units: NumericSuffix::Mm,
8405                }),
8406                y: Expr::Var(Number {
8407                    value: 0.0,
8408                    units: NumericSuffix::Mm,
8409                }),
8410            },
8411            construction: None,
8412        };
8413        let segment = SegmentCtor::Circle(circle_ctor);
8414        let (src_delta, scene_delta) = frontend
8415            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8416            .await
8417            .unwrap();
8418        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8419        // The new objects are start, center, and then the circle segment.
8420        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8421        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8422
8423        let circle = *scene_delta.new_objects.last().unwrap();
8424
8425        // Edit the circle segment.
8426        let circle_ctor = CircleCtor {
8427            start: Point2d {
8428                x: Expr::Var(Number {
8429                    value: 10.0,
8430                    units: NumericSuffix::Mm,
8431                }),
8432                y: Expr::Var(Number {
8433                    value: 0.0,
8434                    units: NumericSuffix::Mm,
8435                }),
8436            },
8437            center: Point2d {
8438                x: Expr::Var(Number {
8439                    value: 3.0,
8440                    units: NumericSuffix::Mm,
8441                }),
8442                y: Expr::Var(Number {
8443                    value: 4.0,
8444                    units: NumericSuffix::Mm,
8445                }),
8446            },
8447            construction: None,
8448        };
8449        let segments = vec![ExistingSegmentCtor {
8450            id: circle,
8451            ctor: SegmentCtor::Circle(circle_ctor),
8452        }];
8453        let (src_delta, scene_delta) = frontend
8454            .edit_segments(&mock_ctx, version, sketch_id, segments)
8455            .await
8456            .unwrap();
8457        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8458        assert_eq!(scene_delta.new_objects, vec![]);
8459        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8460
8461        ctx.close().await;
8462        mock_ctx.close().await;
8463    }
8464
8465    #[tokio::test(flavor = "multi_thread")]
8466    async fn test_delete_circle() {
8467        let initial_source = "sketch001 = sketch(on = XY) {
8468  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8469}
8470";
8471
8472        let program = Program::parse(initial_source).unwrap().0.unwrap();
8473        let mut frontend = FrontendState::new();
8474
8475        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8476        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8477        let version = Version(0);
8478
8479        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8480        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8481        let sketch_id = sketch_object.id;
8482        let sketch = expect_sketch(sketch_object);
8483
8484        // The sketch should have 3 segments: start point, center point, and the circle.
8485        assert_eq!(sketch.segments.len(), 3);
8486        let circle_id = sketch.segments[2];
8487
8488        // Delete the circle.
8489        let (src_delta, scene_delta) = frontend
8490            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8491            .await
8492            .unwrap();
8493        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8494        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8495        let new_sketch = expect_sketch(new_sketch_object);
8496        assert_eq!(new_sketch.segments.len(), 0);
8497
8498        ctx.close().await;
8499        mock_ctx.close().await;
8500    }
8501
8502    #[tokio::test(flavor = "multi_thread")]
8503    async fn test_edit_circle_via_point() {
8504        let initial_source = "sketch001 = sketch(on = XY) {
8505  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8506}
8507";
8508
8509        let program = Program::parse(initial_source).unwrap().0.unwrap();
8510        let mut frontend = FrontendState::new();
8511
8512        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8513        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8514        let version = Version(0);
8515
8516        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8517        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8518        let sketch_id = sketch_object.id;
8519        let sketch = expect_sketch(sketch_object);
8520
8521        // Find the circle segment and its start point.
8522        let circle_id = sketch
8523            .segments
8524            .iter()
8525            .copied()
8526            .find(|seg_id| {
8527                matches!(
8528                    &frontend.scene_graph.objects[seg_id.0].kind,
8529                    ObjectKind::Segment {
8530                        segment: Segment::Circle(_)
8531                    }
8532                )
8533            })
8534            .expect("Expected a circle segment in sketch");
8535        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8536        let ObjectKind::Segment {
8537            segment: Segment::Circle(circle),
8538        } = &circle_object.kind
8539        else {
8540            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8541        };
8542        let start_point_id = circle.start;
8543
8544        // Edit the start point via SegmentCtor::Point.
8545        let segments = vec![ExistingSegmentCtor {
8546            id: start_point_id,
8547            ctor: SegmentCtor::Point(PointCtor {
8548                position: Point2d {
8549                    x: Expr::Var(Number {
8550                        value: 7.0,
8551                        units: NumericSuffix::Mm,
8552                    }),
8553                    y: Expr::Var(Number {
8554                        value: 1.0,
8555                        units: NumericSuffix::Mm,
8556                    }),
8557                },
8558            }),
8559        }];
8560        let (src_delta, _scene_delta) = frontend
8561            .edit_segments(&mock_ctx, version, sketch_id, segments)
8562            .await
8563            .unwrap();
8564        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8565
8566        ctx.close().await;
8567        mock_ctx.close().await;
8568    }
8569
8570    #[tokio::test(flavor = "multi_thread")]
8571    async fn test_add_line_when_sketch_block_uses_variable() {
8572        let initial_source = "s = sketch(on = XY) {}
8573";
8574
8575        let program = Program::parse(initial_source).unwrap().0.unwrap();
8576
8577        let mut frontend = FrontendState::new();
8578
8579        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8580        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8581        let version = Version(0);
8582
8583        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8584        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8585        let sketch_id = sketch_object.id;
8586
8587        let line_ctor = LineCtor {
8588            start: Point2d {
8589                x: Expr::Number(Number {
8590                    value: 0.0,
8591                    units: NumericSuffix::Mm,
8592                }),
8593                y: Expr::Number(Number {
8594                    value: 0.0,
8595                    units: NumericSuffix::Mm,
8596                }),
8597            },
8598            end: Point2d {
8599                x: Expr::Number(Number {
8600                    value: 10.0,
8601                    units: NumericSuffix::Mm,
8602                }),
8603                y: Expr::Number(Number {
8604                    value: 10.0,
8605                    units: NumericSuffix::Mm,
8606                }),
8607            },
8608            construction: None,
8609        };
8610        let segment = SegmentCtor::Line(line_ctor);
8611        let (src_delta, scene_delta) = frontend
8612            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8613            .await
8614            .unwrap();
8615        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8616        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8617        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8618
8619        ctx.close().await;
8620        mock_ctx.close().await;
8621    }
8622
8623    #[tokio::test(flavor = "multi_thread")]
8624    async fn test_new_sketch_add_line_delete_sketch() {
8625        let program = Program::empty();
8626
8627        let mut frontend = FrontendState::new();
8628        frontend.program = program;
8629
8630        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8631        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8632        let version = Version(0);
8633
8634        let sketch_args = SketchCtor {
8635            on: Plane::Default(PlaneName::Xy),
8636        };
8637        let (_src_delta, scene_delta, sketch_id) = frontend
8638            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8639            .await
8640            .unwrap();
8641        assert_eq!(sketch_id, ObjectId(1));
8642        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8643        let sketch_object = &scene_delta.new_graph.objects[1];
8644        assert_eq!(sketch_object.id, ObjectId(1));
8645        assert_eq!(
8646            sketch_object.kind,
8647            ObjectKind::Sketch(Sketch {
8648                args: SketchCtor {
8649                    on: Plane::Default(PlaneName::Xy)
8650                },
8651                plane: ObjectId(0),
8652                segments: vec![],
8653                constraints: vec![],
8654            })
8655        );
8656        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8657
8658        let line_ctor = LineCtor {
8659            start: Point2d {
8660                x: Expr::Number(Number {
8661                    value: 0.0,
8662                    units: NumericSuffix::Mm,
8663                }),
8664                y: Expr::Number(Number {
8665                    value: 0.0,
8666                    units: NumericSuffix::Mm,
8667                }),
8668            },
8669            end: Point2d {
8670                x: Expr::Number(Number {
8671                    value: 10.0,
8672                    units: NumericSuffix::Mm,
8673                }),
8674                y: Expr::Number(Number {
8675                    value: 10.0,
8676                    units: NumericSuffix::Mm,
8677                }),
8678            },
8679            construction: None,
8680        };
8681        let segment = SegmentCtor::Line(line_ctor);
8682        let (src_delta, scene_delta) = frontend
8683            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8684            .await
8685            .unwrap();
8686        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8687        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8688
8689        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8690        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8691        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8692
8693        ctx.close().await;
8694        mock_ctx.close().await;
8695    }
8696
8697    #[tokio::test(flavor = "multi_thread")]
8698    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8699        let initial_source = "s = sketch(on = XY) {}
8700";
8701
8702        let program = Program::parse(initial_source).unwrap().0.unwrap();
8703
8704        let mut frontend = FrontendState::new();
8705
8706        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8707        let version = Version(0);
8708
8709        frontend.hack_set_program(&ctx, program).await.unwrap();
8710        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8711        let sketch_id = sketch_object.id;
8712
8713        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8714        insta::assert_snapshot!(
8715            "test_delete_sketch_when_sketch_block_uses_variable",
8716            src_delta.text.as_str()
8717        );
8718        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8719
8720        ctx.close().await;
8721    }
8722
8723    #[tokio::test(flavor = "multi_thread")]
8724    async fn test_delete_sketch_after_comment() {
8725        let initial_source = "sketch001 = sketch(on = XZ) {
8726}
8727";
8728
8729        let program = Program::parse(initial_source).unwrap().0.unwrap();
8730        let mut frontend = FrontendState::new();
8731
8732        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8733        let version = Version(0);
8734
8735        frontend.hack_set_program(&ctx, program).await.unwrap();
8736        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8737        let sketch_id = sketch_object.id;
8738        let original_source = sketch_object.source.clone();
8739
8740        let commented_source = "// test 1
8741sketch001 = sketch(on = XZ) {
8742}
8743";
8744        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8745        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8746
8747        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8748        assert_eq!(cached_sketch_object.source, original_source);
8749
8750        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8751        assert!(
8752            !src_delta.text.contains("sketch001"),
8753            "sketch was not deleted: {}",
8754            src_delta.text
8755        );
8756        // The leading line comment must survive deletion.
8757        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8758        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8759
8760        ctx.close().await;
8761    }
8762
8763    #[tokio::test(flavor = "multi_thread")]
8764    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8765        let initial_source = "sketch001 = sketch(on = XZ) {
8766}
8767foo = 1
8768";
8769
8770        let program = Program::parse(initial_source).unwrap().0.unwrap();
8771        let mut frontend = FrontendState::new();
8772
8773        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8774        let version = Version(0);
8775
8776        frontend.hack_set_program(&ctx, program).await.unwrap();
8777        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8778        let sketch_id = sketch_object.id;
8779
8780        let commented_source = "// keep me
8781sketch001 = sketch(on = XZ) {
8782}
8783foo = 1
8784";
8785        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8786        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8787
8788        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8789        // The leading comment should remain, now attached to the following body item.
8790        insta::assert_snapshot!(
8791            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8792            src_delta.text.as_str()
8793        );
8794
8795        ctx.close().await;
8796    }
8797
8798    #[tokio::test(flavor = "multi_thread")]
8799    async fn test_delete_segment_preserves_pre_comment() {
8800        let initial_source = "\
8801sketch(on = XY) {
8802  point(at = [var 1, var 2])
8803  // describe the middle point
8804  point(at = [var 3, var 4])
8805  point(at = [var 5, var 6])
8806}
8807";
8808
8809        let program = Program::parse(initial_source).unwrap().0.unwrap();
8810        let mut frontend = FrontendState::new();
8811
8812        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8813        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8814        let version = Version(0);
8815
8816        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8817        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8818        let sketch_id = sketch_object.id;
8819        let sketch = expect_sketch(sketch_object);
8820
8821        let middle_point_id = *sketch.segments.get(1).unwrap();
8822
8823        let (src_delta, _scene_delta) = frontend
8824            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8825            .await
8826            .unwrap();
8827        // The line comment on the line above the deleted point must be preserved.
8828        // It is reattached to the next surviving body item.
8829        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8830
8831        ctx.close().await;
8832        mock_ctx.close().await;
8833    }
8834
8835    #[tokio::test(flavor = "multi_thread")]
8836    async fn test_delete_last_segment_preserves_pre_comment() {
8837        let initial_source = "\
8838sketch(on = XY) {
8839  point(at = [var 1, var 2])
8840  // describe the trailing point
8841  point(at = [var 3, var 4])
8842}
8843";
8844
8845        let program = Program::parse(initial_source).unwrap().0.unwrap();
8846        let mut frontend = FrontendState::new();
8847
8848        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8849        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8850        let version = Version(0);
8851
8852        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8853        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8854        let sketch_id = sketch_object.id;
8855        let sketch = expect_sketch(sketch_object);
8856
8857        let last_point_id = *sketch.segments.last().unwrap();
8858
8859        let (src_delta, _scene_delta) = frontend
8860            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8861            .await
8862            .unwrap();
8863        // No following item to attach to; the comment is kept inside the sketch
8864        // block as trailing non-code metadata so the user does not lose it.
8865        insta::assert_snapshot!(
8866            "test_delete_last_segment_preserves_pre_comment",
8867            src_delta.text.as_str()
8868        );
8869
8870        ctx.close().await;
8871        mock_ctx.close().await;
8872    }
8873
8874    #[tokio::test(flavor = "multi_thread")]
8875    async fn test_delete_segment_drops_inline_trailing_comment() {
8876        let initial_source = "\
8877sketch(on = XY) {
8878  point(at = [var 1, var 2])
8879  point(at = [var 3, var 4]) // same-line note that gets dropped
8880  point(at = [var 5, var 6])
8881}
8882";
8883
8884        let program = Program::parse(initial_source).unwrap().0.unwrap();
8885        let mut frontend = FrontendState::new();
8886
8887        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8888        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8889        let version = Version(0);
8890
8891        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8892        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8893        let sketch_id = sketch_object.id;
8894        let sketch = expect_sketch(sketch_object);
8895
8896        let middle_point_id = *sketch.segments.get(1).unwrap();
8897
8898        let (src_delta, _scene_delta) = frontend
8899            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8900            .await
8901            .unwrap();
8902        // The same-line trailing comment is removed along with the deleted code.
8903        assert!(
8904            !src_delta.text.contains("same-line note"),
8905            "inline comment should have been removed: {}",
8906            src_delta.text
8907        );
8908
8909        ctx.close().await;
8910        mock_ctx.close().await;
8911    }
8912
8913    #[tokio::test(flavor = "multi_thread")]
8914    async fn test_delete_segments_preserves_block_comments_across_positions() {
8915        // One test exercising several `delete_body_item_preserving_pre_comments`
8916        // branches at once with `/* ... */` block comments:
8917        //   - first point: leading block comment must migrate to the next item.
8918        //   - first point: same-line trailing block comment must be dropped.
8919        //   - middle point: leading block comment must stay attached after migration.
8920        //   - last point: leading block comment, with no surviving next item,
8921        //     must be converted into a trailing NonCodeNode.
8922        let initial_source = "\
8923sketch(on = XY) {
8924  /* above first - moves to middle */
8925  point(at = [var 1, var 2]) /* same-line on first - dropped */
8926  /* above middle - stays */
8927  point(at = [var 3, var 4])
8928  /* above last - moves to trailing meta */
8929  point(at = [var 5, var 6])
8930}
8931";
8932
8933        let program = Program::parse(initial_source).unwrap().0.unwrap();
8934        let mut frontend = FrontendState::new();
8935
8936        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8937        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8938        let version = Version(0);
8939
8940        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8941        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8942        let sketch_id = sketch_object.id;
8943        let sketch = expect_sketch(sketch_object);
8944
8945        let first_point_id = *sketch.segments.first().unwrap();
8946        let last_point_id = *sketch.segments.last().unwrap();
8947
8948        let (src_delta, _scene_delta) = frontend
8949            .delete_objects(
8950                &mock_ctx,
8951                version,
8952                sketch_id,
8953                Vec::new(),
8954                vec![first_point_id, last_point_id],
8955            )
8956            .await
8957            .unwrap();
8958        insta::assert_snapshot!(
8959            "test_delete_segments_preserves_block_comments_across_positions",
8960            src_delta.text.as_str()
8961        );
8962
8963        ctx.close().await;
8964        mock_ctx.close().await;
8965    }
8966
8967    #[tokio::test(flavor = "multi_thread")]
8968    async fn test_edit_line_when_editing_its_start_point() {
8969        let initial_source = "\
8970sketch(on = XY) {
8971  line(start = [var 1, var 2], end = [var 3, var 4])
8972}
8973";
8974
8975        let program = Program::parse(initial_source).unwrap().0.unwrap();
8976
8977        let mut frontend = FrontendState::new();
8978
8979        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8980        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8981        let version = Version(0);
8982
8983        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8984        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8985        let sketch_id = sketch_object.id;
8986        let sketch = expect_sketch(sketch_object);
8987
8988        let point_id = *sketch.segments.first().unwrap();
8989
8990        let point_ctor = PointCtor {
8991            position: Point2d {
8992                x: Expr::Var(Number {
8993                    value: 5.0,
8994                    units: NumericSuffix::Inch,
8995                }),
8996                y: Expr::Var(Number {
8997                    value: 6.0,
8998                    units: NumericSuffix::Inch,
8999                }),
9000            },
9001        };
9002        let segments = vec![ExistingSegmentCtor {
9003            id: point_id,
9004            ctor: SegmentCtor::Point(point_ctor),
9005        }];
9006        let (src_delta, scene_delta) = frontend
9007            .edit_segments(&mock_ctx, version, sketch_id, segments)
9008            .await
9009            .unwrap();
9010        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9011        assert_eq!(scene_delta.new_objects, vec![]);
9012        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9013
9014        ctx.close().await;
9015        mock_ctx.close().await;
9016    }
9017
9018    #[tokio::test(flavor = "multi_thread")]
9019    async fn test_edit_line_when_editing_its_end_point() {
9020        let initial_source = "\
9021sketch(on = XY) {
9022  line(start = [var 1, var 2], end = [var 3, var 4])
9023}
9024";
9025
9026        let program = Program::parse(initial_source).unwrap().0.unwrap();
9027
9028        let mut frontend = FrontendState::new();
9029
9030        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9031        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9032        let version = Version(0);
9033
9034        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9035        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9036        let sketch_id = sketch_object.id;
9037        let sketch = expect_sketch(sketch_object);
9038        let point_id = *sketch.segments.get(1).unwrap();
9039
9040        let point_ctor = PointCtor {
9041            position: Point2d {
9042                x: Expr::Var(Number {
9043                    value: 5.0,
9044                    units: NumericSuffix::Inch,
9045                }),
9046                y: Expr::Var(Number {
9047                    value: 6.0,
9048                    units: NumericSuffix::Inch,
9049                }),
9050            },
9051        };
9052        let segments = vec![ExistingSegmentCtor {
9053            id: point_id,
9054            ctor: SegmentCtor::Point(point_ctor),
9055        }];
9056        let (src_delta, scene_delta) = frontend
9057            .edit_segments(&mock_ctx, version, sketch_id, segments)
9058            .await
9059            .unwrap();
9060        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9061        assert_eq!(scene_delta.new_objects, vec![]);
9062        assert_eq!(
9063            scene_delta.new_graph.objects.len(),
9064            5,
9065            "{:#?}",
9066            scene_delta.new_graph.objects
9067        );
9068
9069        ctx.close().await;
9070        mock_ctx.close().await;
9071    }
9072
9073    #[tokio::test(flavor = "multi_thread")]
9074    async fn test_edit_line_with_coincident_feedback() {
9075        let initial_source = "\
9076sketch(on = XY) {
9077  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9078  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9079  fixed([line1.start, [0, 0]])
9080  coincident([line1.end, line2.start])
9081  equalLength([line1, line2])
9082}
9083";
9084
9085        let program = Program::parse(initial_source).unwrap().0.unwrap();
9086
9087        let mut frontend = FrontendState::new();
9088
9089        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9090        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9091        let version = Version(0);
9092
9093        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9094        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9095        let sketch_id = sketch_object.id;
9096        let sketch = expect_sketch(sketch_object);
9097        let line2_end_id = *sketch.segments.get(4).unwrap();
9098
9099        let segments = vec![ExistingSegmentCtor {
9100            id: line2_end_id,
9101            ctor: SegmentCtor::Point(PointCtor {
9102                position: Point2d {
9103                    x: Expr::Var(Number {
9104                        value: 9.0,
9105                        units: NumericSuffix::None,
9106                    }),
9107                    y: Expr::Var(Number {
9108                        value: 10.0,
9109                        units: NumericSuffix::None,
9110                    }),
9111                },
9112            }),
9113        }];
9114        let (src_delta, scene_delta) = frontend
9115            .edit_segments(&mock_ctx, version, sketch_id, segments)
9116            .await
9117            .unwrap();
9118        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9119        assert_eq!(
9120            scene_delta.new_graph.objects.len(),
9121            11,
9122            "{:#?}",
9123            scene_delta.new_graph.objects
9124        );
9125
9126        ctx.close().await;
9127        mock_ctx.close().await;
9128    }
9129
9130    #[tokio::test(flavor = "multi_thread")]
9131    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9132        let initial_source = "\
9133sketch(on = XY) {
9134  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9135  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9136  fixed([line1.start, [0, 0]])
9137  coincident([line1.end, line2.start])
9138  equalLength([line1, line2])
9139}
9140";
9141
9142        let program = Program::parse(initial_source).unwrap().0.unwrap();
9143        let mut frontend = FrontendState::new();
9144        let mock_ctx = ExecutorContext::new_mock(None).await;
9145        let version = Version(0);
9146
9147        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9148        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9149        let sketch_id = sketch_object.id;
9150        let sketch = expect_sketch(sketch_object);
9151        let line2_end_id = *sketch.segments.get(4).unwrap();
9152
9153        let segments = vec![ExistingSegmentCtor {
9154            id: line2_end_id,
9155            ctor: SegmentCtor::Point(PointCtor {
9156                position: Point2d {
9157                    x: Expr::Var(Number {
9158                        value: 9.0,
9159                        units: NumericSuffix::None,
9160                    }),
9161                    y: Expr::Var(Number {
9162                        value: 10.0,
9163                        units: NumericSuffix::None,
9164                    }),
9165                },
9166            }),
9167        }];
9168        let (edited_source, _) = frontend
9169            .edit_segments(&mock_ctx, version, sketch_id, segments)
9170            .await
9171            .unwrap();
9172
9173        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9174        assert_eq!(mock_source.text, edited_source.text);
9175
9176        mock_ctx.close().await;
9177    }
9178
9179    /// Preview segment edits should return solved geometry without persisting
9180    /// solver feedback to KCL.
9181    #[tokio::test(flavor = "multi_thread")]
9182    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9183        let initial_source = "\
9184sketch(on = XY) {
9185  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9186  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9187  fixed([line1.start, [0, 0]])
9188  coincident([line1.end, line2.start])
9189  equalLength([line1, line2])
9190}
9191";
9192
9193        let program = Program::parse(initial_source).unwrap().0.unwrap();
9194        let mut frontend = FrontendState::new();
9195        let mock_ctx = ExecutorContext::new_mock(None).await;
9196        let version = Version(0);
9197
9198        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9199        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9200        let sketch_id = sketch_object.id;
9201        let sketch = expect_sketch(sketch_object);
9202        let line2_end_id = *sketch.segments.get(4).unwrap();
9203
9204        let segments = vec![ExistingSegmentCtor {
9205            id: line2_end_id,
9206            ctor: SegmentCtor::Point(PointCtor {
9207                position: Point2d {
9208                    x: Expr::Var(Number {
9209                        value: 9.0,
9210                        units: NumericSuffix::None,
9211                    }),
9212                    y: Expr::Var(Number {
9213                        value: 10.0,
9214                        units: NumericSuffix::None,
9215                    }),
9216                },
9217            }),
9218        }];
9219        let (preview_source, preview_delta) = frontend
9220            .edit_segments_with_options(
9221                &mock_ctx,
9222                version,
9223                sketch_id,
9224                segments,
9225                EditSegmentsOptions {
9226                    anchor_segment_ids: Some(vec![line2_end_id]),
9227                    drag_anchors: Vec::new(),
9228                    constraint_label_edits: Vec::new(),
9229                    commit_solved_initial_guesses: false,
9230                },
9231            )
9232            .await
9233            .unwrap();
9234
9235        assert!(
9236            !preview_delta.exec_outcome.var_solutions.is_empty(),
9237            "preview solve should still solve and return geometry feedback"
9238        );
9239        assert!(
9240            preview_source
9241                .text
9242                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9243        );
9244        assert!(
9245            preview_source
9246                .text
9247                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9248        );
9249
9250        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9251        assert_eq!(mock_source.text, preview_source.text);
9252
9253        mock_ctx.close().await;
9254    }
9255
9256    #[tokio::test(flavor = "multi_thread")]
9257    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9258        let initial_source = "\
9259sketch(on = XY) {
9260  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9261}
9262";
9263
9264        let program = Program::parse(initial_source).unwrap().0.unwrap();
9265        let mut frontend = FrontendState::new();
9266        let mock_ctx = ExecutorContext::new_mock(None).await;
9267        let version = Version(0);
9268
9269        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9271        let sketch_id = sketch_object.id;
9272        let sketch = expect_sketch(sketch_object);
9273        let line_end_id = *sketch.segments.get(1).unwrap();
9274
9275        let constraint = Constraint::Fixed(Fixed {
9276            points: vec![FixedPoint {
9277                point: line_end_id,
9278                position: Point2d {
9279                    x: Number {
9280                        value: 20.0,
9281                        units: NumericSuffix::Mm,
9282                    },
9283                    y: Number {
9284                        value: 0.0,
9285                        units: NumericSuffix::Mm,
9286                    },
9287                },
9288            }],
9289        });
9290        let (constraint_source, _) = frontend
9291            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9292            .await
9293            .unwrap();
9294
9295        assert!(
9296            constraint_source
9297                .text
9298                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9299            "{}",
9300            constraint_source.text
9301        );
9302        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9303        assert_eq!(mock_source.text, constraint_source.text);
9304
9305        mock_ctx.close().await;
9306    }
9307
9308    #[test]
9309    fn test_no_solver_feedback_preserves_original_source() {
9310        let initial_source = "\
9311@settings(defaultLengthUnit = in, kclVersion = 2.0)
9312cylinder = startSketchOn(XY)
9313    |> circle(center= [0, 0], radius= 22)
9314    |> extrude(length = 14)
9315";
9316        let mut frontend = FrontendState::new();
9317        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9318        let outcome = ExecOutcome {
9319            variables: Default::default(),
9320            test_program_memory: Default::default(),
9321            operations: Default::default(),
9322            artifact_graph: Default::default(),
9323            scene_objects: Default::default(),
9324            source_range_to_object: Default::default(),
9325            var_solutions: Default::default(),
9326            refactor_metadata: Default::default(),
9327            issues: Default::default(),
9328            filenames: Default::default(),
9329            from_cache: false,
9330            source_files: Default::default(),
9331            default_planes: Default::default(),
9332        };
9333
9334        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9335
9336        assert_eq!(source_delta.text, initial_source);
9337    }
9338
9339    /// Explicit drag anchors should limit which edited points become temporary
9340    /// fixed constraints.
9341    #[tokio::test(flavor = "multi_thread")]
9342    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9343        let initial_source = "\
9344sketch(on = XY) {
9345  point1 = point(at = [var 0mm, var 0mm])
9346  point2 = point(at = [var 0mm, var 0mm])
9347  coincident([point1, point2])
9348}
9349";
9350
9351        let program = Program::parse(initial_source).unwrap().0.unwrap();
9352        let mut frontend = FrontendState::new();
9353        let mock_ctx = ExecutorContext::new_mock(None).await;
9354        let version = Version(0);
9355
9356        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9357        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9358        let sketch_id = sketch_object.id;
9359        let sketch = expect_sketch(sketch_object);
9360        let point1_id = sketch.segments[0];
9361        let point2_id = sketch.segments[1];
9362
9363        let segments = vec![
9364            ExistingSegmentCtor {
9365                id: point1_id,
9366                ctor: SegmentCtor::Point(PointCtor {
9367                    position: point_expr_mm(10.0, 0.0),
9368                }),
9369            },
9370            ExistingSegmentCtor {
9371                id: point2_id,
9372                ctor: SegmentCtor::Point(PointCtor {
9373                    position: point_expr_mm(100.0, 0.0),
9374                }),
9375            },
9376        ];
9377        let (_, scene_delta) = frontend
9378            .edit_segments_with_options(
9379                &mock_ctx,
9380                version,
9381                sketch_id,
9382                segments,
9383                EditSegmentsOptions {
9384                    anchor_segment_ids: Some(vec![point1_id]),
9385                    drag_anchors: Vec::new(),
9386                    constraint_label_edits: Vec::new(),
9387                    commit_solved_initial_guesses: true,
9388                },
9389            )
9390            .await
9391            .unwrap();
9392
9393        assert_point_position_close(
9394            point_position(&scene_delta.new_graph, point1_id),
9395            point_number_mm(10.0, 0.0),
9396        );
9397        assert_point_position_close(
9398            point_position(&scene_delta.new_graph, point2_id),
9399            point_number_mm(10.0, 0.0),
9400        );
9401
9402        mock_ctx.close().await;
9403    }
9404
9405    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9406    /// for every SketchVar whose initial NumericLiteral has the given value.
9407    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9408        use std::cell::RefCell;
9409        struct Collector {
9410            target: f64,
9411            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9412        }
9413        impl<'a> crate::walk::Visitor<'a> for &Collector {
9414            type Error = crate::front::Error;
9415            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9416                if let crate::walk::Node::SketchVar(sketch_var) = node
9417                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9418                    && (initial.value - self.target).abs() < 1e-9
9419                {
9420                    self.out
9421                        .borrow_mut()
9422                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9423                }
9424                for child in node.children().iter() {
9425                    if !child.visit(*self)? {
9426                        return Ok(false);
9427                    }
9428                }
9429                Ok(true)
9430            }
9431        }
9432        let collector = Collector {
9433            target: value,
9434            out: Default::default(),
9435        };
9436        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9437        collector.out.into_inner()
9438    }
9439
9440    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9441    /// for every SketchVar (including bare `var`).
9442    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9443        use std::cell::RefCell;
9444        struct Collector {
9445            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9446        }
9447        impl<'a> crate::walk::Visitor<'a> for &Collector {
9448            type Error = crate::front::Error;
9449            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9450                if let crate::walk::Node::SketchVar(sketch_var) = node
9451                    && let Some(node_path) = &sketch_var.node_path
9452                {
9453                    self.out
9454                        .borrow_mut()
9455                        .push((SourceRange::from(sketch_var), node_path.clone()));
9456                }
9457                for child in node.children().iter() {
9458                    if !child.visit(*self)? {
9459                        return Ok(false);
9460                    }
9461                }
9462                Ok(true)
9463            }
9464        }
9465        let collector = Collector {
9466            out: Default::default(),
9467        };
9468        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9469        collector.out.into_inner()
9470    }
9471
9472    fn empty_exec_outcome_with_var_solutions(
9473        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9474    ) -> ExecOutcome {
9475        ExecOutcome {
9476            variables: Default::default(),
9477            test_program_memory: Default::default(),
9478            operations: Default::default(),
9479            artifact_graph: Default::default(),
9480            scene_objects: Default::default(),
9481            source_range_to_object: Default::default(),
9482            var_solutions,
9483            refactor_metadata: Default::default(),
9484            issues: Default::default(),
9485            filenames: Default::default(),
9486            from_cache: false,
9487            source_files: Default::default(),
9488            default_planes: Default::default(),
9489        }
9490    }
9491
9492    /// Happy path: commit a var solution to a `var N` inside a sketch block
9493    /// using a correct NodePath. Confirms the node-path code path produces the
9494    /// expected source mutation.
9495    #[test]
9496    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9497        let initial_source = "\
9498sketch(on = XY) {
9499  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9500}
9501";
9502        let program = Program::parse(initial_source).unwrap().0.unwrap();
9503        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9504        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9505        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9506
9507        let mut frontend = FrontendState::new();
9508        frontend.program = program;
9509
9510        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9511            literal_range,
9512            Some(node_path),
9513            Number {
9514                value: 25.0,
9515                units: NumericSuffix::Mm,
9516            },
9517        )]);
9518
9519        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9520
9521        insta::assert_snapshot!(
9522            "test_commit_var_solution_by_node_path_updates_sketch_var",
9523            source_delta.text
9524        );
9525    }
9526
9527    /// Whitespace inserted earlier in the source shifts the original SketchVar
9528    /// SourceRange. With NodePath propagation the commit should still target
9529    /// the right `var`. We simulate this by collecting node_paths against a
9530    /// "compact" source, then loading the frontend with a "padded" source
9531    /// (whose byte offsets differ), and feeding the original (now stale)
9532    /// source range plus the correct node_path back into the commit.
9533    #[test]
9534    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9535        let compact_source = "\
9536sketch(on = XY) {
9537  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9538}
9539";
9540        let padded_source = "\
9541// added comment\n// added comment\n\nsketch(on = XY) {
9542  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9543}
9544";
9545        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9546        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9547
9548        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9549            .into_iter()
9550            .next()
9551            .expect("expected `var 10mm` in compact source");
9552        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9553            .into_iter()
9554            .next()
9555            .expect("expected `var 10mm` in padded source");
9556
9557        assert_ne!(
9558            compact_match.0, padded_match.0,
9559            "byte offsets must differ for this test to be meaningful"
9560        );
9561        assert_eq!(
9562            compact_match.1, padded_match.1,
9563            "node paths must agree across whitespace; that's the whole point of NodePath",
9564        );
9565
9566        let mut frontend = FrontendState::new();
9567        frontend.program = padded_program;
9568
9569        // Stale source range from the compact source + correct node_path.
9570        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9571            compact_match.0,
9572            Some(compact_match.1),
9573            Number {
9574                value: 30.0,
9575                units: NumericSuffix::Mm,
9576            },
9577        )]);
9578
9579        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9580
9581        insta::assert_snapshot!(
9582            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9583            source_delta.text
9584        );
9585    }
9586
9587    /// When multiple `var` declarations exist and the stale source range
9588    /// happens to land on a *different* var, the node_path must take
9589    /// precedence and the right var gets updated.
9590    #[test]
9591    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9592        let initial_source = "\
9593sketch(on = XY) {
9594  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9595}
9596";
9597        let program = Program::parse(initial_source).unwrap().0.unwrap();
9598
9599        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9600            .into_iter()
9601            .next()
9602            .expect("expected `var 10mm`");
9603        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9604            .into_iter()
9605            .next()
9606            .expect("expected `var 20mm`");
9607
9608        let mut frontend = FrontendState::new();
9609        frontend.program = program;
9610
9611        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9612        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9613            var_20.0,
9614            Some(var_10.1),
9615            Number {
9616                value: 33.0,
9617                units: NumericSuffix::Mm,
9618            },
9619        )]);
9620
9621        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9622
9623        insta::assert_snapshot!(
9624            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9625            source_delta.text
9626        );
9627    }
9628
9629    /// Bare `var` (no initial literal) is only locatable via node_path. With
9630    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9631    /// solver solution should fill the initial value in. The
9632    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9633    /// is gated as an experimental feature; without it the re-parse of the
9634    /// recast source rejects bare `var` declarations.
9635    #[test]
9636    fn test_commit_var_solution_writes_back_into_bare_var() {
9637        let initial_source = "\
9638@settings(experimentalFeatures = allow, kclVersion = 2.0)
9639sketch(on = XY) {
9640  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9641}
9642";
9643        let program = Program::parse(initial_source).unwrap().0.unwrap();
9644
9645        // Pick the first bare `var`; collect_all_sketch_vars returns every
9646        // SketchVar, including bare ones.
9647        let bare = collect_all_sketch_vars(&program)
9648            .into_iter()
9649            .find(|(range, _)| {
9650                // The bare `var` is exactly the 3 characters "var".
9651                range.end() - range.start() == 3
9652            })
9653            .expect("expected at least one bare `var`");
9654
9655        let mut frontend = FrontendState::new();
9656        frontend.program = program;
9657
9658        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9659            bare.0,
9660            Some(bare.1),
9661            Number {
9662                value: 7.0,
9663                units: NumericSuffix::Mm,
9664            },
9665        )]);
9666
9667        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9668
9669        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9670        // written as an explicit suffix so the bare var commits with units.
9671        // The recast adds a blank line after the `@settings` annotation.
9672        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9673    }
9674
9675    #[tokio::test(flavor = "multi_thread")]
9676    async fn test_delete_point_without_var() {
9677        let initial_source = "\
9678sketch(on = XY) {
9679  point(at = [var 1, var 2])
9680  point(at = [var 3, var 4])
9681  point(at = [var 5, var 6])
9682}
9683";
9684
9685        let program = Program::parse(initial_source).unwrap().0.unwrap();
9686
9687        let mut frontend = FrontendState::new();
9688
9689        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9690        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9691        let version = Version(0);
9692
9693        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9695        let sketch_id = sketch_object.id;
9696        let sketch = expect_sketch(sketch_object);
9697
9698        let point_id = *sketch.segments.get(1).unwrap();
9699
9700        let (src_delta, scene_delta) = frontend
9701            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9702            .await
9703            .unwrap();
9704        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9705        assert_eq!(scene_delta.new_objects, vec![]);
9706        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9707
9708        ctx.close().await;
9709        mock_ctx.close().await;
9710    }
9711
9712    #[tokio::test(flavor = "multi_thread")]
9713    async fn test_delete_point_with_var() {
9714        let initial_source = "\
9715sketch(on = XY) {
9716  point(at = [var 1, var 2])
9717  point1 = point(at = [var 3, var 4])
9718  point(at = [var 5, var 6])
9719}
9720";
9721
9722        let program = Program::parse(initial_source).unwrap().0.unwrap();
9723
9724        let mut frontend = FrontendState::new();
9725
9726        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9727        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9728        let version = Version(0);
9729
9730        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9731        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9732        let sketch_id = sketch_object.id;
9733        let sketch = expect_sketch(sketch_object);
9734
9735        let point_id = *sketch.segments.get(1).unwrap();
9736
9737        let (src_delta, scene_delta) = frontend
9738            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9739            .await
9740            .unwrap();
9741        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9742        assert_eq!(scene_delta.new_objects, vec![]);
9743        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9744
9745        ctx.close().await;
9746        mock_ctx.close().await;
9747    }
9748
9749    #[tokio::test(flavor = "multi_thread")]
9750    async fn test_delete_multiple_points() {
9751        let initial_source = "\
9752sketch(on = XY) {
9753  point(at = [var 1, var 2])
9754  point1 = point(at = [var 3, var 4])
9755  point(at = [var 5, var 6])
9756}
9757";
9758
9759        let program = Program::parse(initial_source).unwrap().0.unwrap();
9760
9761        let mut frontend = FrontendState::new();
9762
9763        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9764        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9765        let version = Version(0);
9766
9767        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9768        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9769        let sketch_id = sketch_object.id;
9770
9771        let sketch = expect_sketch(sketch_object);
9772
9773        let point1_id = *sketch.segments.first().unwrap();
9774        let point2_id = *sketch.segments.get(1).unwrap();
9775
9776        let (src_delta, scene_delta) = frontend
9777            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9778            .await
9779            .unwrap();
9780        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9781        assert_eq!(scene_delta.new_objects, vec![]);
9782        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9783
9784        ctx.close().await;
9785        mock_ctx.close().await;
9786    }
9787
9788    #[tokio::test(flavor = "multi_thread")]
9789    async fn test_delete_coincident_constraint() {
9790        let initial_source = "\
9791sketch(on = XY) {
9792  point1 = point(at = [var 1, var 2])
9793  point2 = point(at = [var 3, var 4])
9794  coincident([point1, point2])
9795  point(at = [var 5, var 6])
9796}
9797";
9798
9799        let program = Program::parse(initial_source).unwrap().0.unwrap();
9800
9801        let mut frontend = FrontendState::new();
9802
9803        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9804        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9805        let version = Version(0);
9806
9807        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9808        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9809        let sketch_id = sketch_object.id;
9810        let sketch = expect_sketch(sketch_object);
9811
9812        let coincident_id = *sketch.constraints.first().unwrap();
9813
9814        let (src_delta, scene_delta) = frontend
9815            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9816            .await
9817            .unwrap();
9818        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9819        assert_eq!(scene_delta.new_objects, vec![]);
9820        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9821
9822        ctx.close().await;
9823        mock_ctx.close().await;
9824    }
9825
9826    #[tokio::test(flavor = "multi_thread")]
9827    async fn test_delete_line_cascades_to_coincident_constraint() {
9828        let initial_source = "\
9829sketch(on = XY) {
9830  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9831  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9832  coincident([line1.end, line2.start])
9833}
9834";
9835
9836        let program = Program::parse(initial_source).unwrap().0.unwrap();
9837
9838        let mut frontend = FrontendState::new();
9839
9840        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9841        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9842        let version = Version(0);
9843
9844        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9845        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9846        let sketch_id = sketch_object.id;
9847        let sketch = expect_sketch(sketch_object);
9848        let line_id = *sketch.segments.get(5).unwrap();
9849
9850        let (src_delta, scene_delta) = frontend
9851            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9852            .await
9853            .unwrap();
9854        insta::assert_snapshot!(
9855            "test_delete_line_cascades_to_coincident_constraint",
9856            src_delta.text.as_str()
9857        );
9858        assert_eq!(
9859            scene_delta.new_graph.objects.len(),
9860            5,
9861            "{:#?}",
9862            scene_delta.new_graph.objects
9863        );
9864
9865        ctx.close().await;
9866        mock_ctx.close().await;
9867    }
9868
9869    #[tokio::test(flavor = "multi_thread")]
9870    async fn test_delete_line_cascades_to_distance_constraint() {
9871        let initial_source = "\
9872sketch(on = XY) {
9873  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9874  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9875  distance([line1.end, line2.start]) == 10mm
9876}
9877";
9878
9879        let program = Program::parse(initial_source).unwrap().0.unwrap();
9880
9881        let mut frontend = FrontendState::new();
9882
9883        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9884        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9885        let version = Version(0);
9886
9887        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9888        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9889        let sketch_id = sketch_object.id;
9890        let sketch = expect_sketch(sketch_object);
9891        let line_id = *sketch.segments.get(5).unwrap();
9892
9893        let (src_delta, scene_delta) = frontend
9894            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9895            .await
9896            .unwrap();
9897        insta::assert_snapshot!(
9898            "test_delete_line_cascades_to_distance_constraint",
9899            src_delta.text.as_str()
9900        );
9901        assert_eq!(
9902            scene_delta.new_graph.objects.len(),
9903            5,
9904            "{:#?}",
9905            scene_delta.new_graph.objects
9906        );
9907
9908        ctx.close().await;
9909        mock_ctx.close().await;
9910    }
9911
9912    #[tokio::test(flavor = "multi_thread")]
9913    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9914        let initial_source = "\
9915sketch(on = XY) {
9916  point1 = point(at = [var 1, var 2])
9917  point2 = point(at = [var 3, var 4])
9918  horizontalDistance([point1, point2]) == 10mm
9919}
9920";
9921
9922        let program = Program::parse(initial_source).unwrap().0.unwrap();
9923
9924        let mut frontend = FrontendState::new();
9925
9926        let mock_ctx = ExecutorContext::new_mock(None).await;
9927        let version = Version(0);
9928
9929        frontend.program = program.clone();
9930        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9931        frontend.update_state_after_exec(outcome, true);
9932        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9933        let sketch_id = sketch_object.id;
9934        let sketch = expect_sketch(sketch_object);
9935        let point2_id = *sketch.segments.get(1).unwrap();
9936
9937        let (src_delta, scene_delta) = frontend
9938            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9939            .await
9940            .unwrap();
9941        insta::assert_snapshot!(
9942            "test_delete_point_cascades_to_horizontal_distance_constraint",
9943            src_delta.text.as_str()
9944        );
9945        assert_eq!(
9946            scene_delta.new_graph.objects.len(),
9947            3,
9948            "{:#?}",
9949            scene_delta.new_graph.objects
9950        );
9951
9952        mock_ctx.close().await;
9953    }
9954
9955    #[tokio::test(flavor = "multi_thread")]
9956    async fn test_delete_line_cascades_to_fixed_constraint() {
9957        let initial_source = "\
9958sketch(on = XY) {
9959  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9960  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9961  fixed([line1.start, [0, 0]])
9962}
9963";
9964
9965        let program = Program::parse(initial_source).unwrap().0.unwrap();
9966
9967        let mut frontend = FrontendState::new();
9968
9969        let mock_ctx = ExecutorContext::new_mock(None).await;
9970        let version = Version(0);
9971
9972        frontend.program = program.clone();
9973        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9974        frontend.update_state_after_exec(outcome, true);
9975        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9976        let sketch_id = sketch_object.id;
9977        let sketch = expect_sketch(sketch_object);
9978        let line1_id = *sketch.segments.get(2).unwrap();
9979
9980        let (src_delta, scene_delta) = frontend
9981            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9982            .await
9983            .unwrap();
9984        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9985        assert_eq!(
9986            scene_delta.new_graph.objects.len(),
9987            5,
9988            "{:#?}",
9989            scene_delta.new_graph.objects
9990        );
9991
9992        mock_ctx.close().await;
9993    }
9994
9995    #[tokio::test(flavor = "multi_thread")]
9996    async fn test_delete_line_cascades_to_midpoint_constraint() {
9997        let initial_source = "\
9998sketch(on = XY) {
9999  point1 = point(at = [var 1, var 2])
10000  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10001  midpoint(line1, point = point1)
10002}
10003";
10004
10005        let program = Program::parse(initial_source).unwrap().0.unwrap();
10006
10007        let mut frontend = FrontendState::new();
10008
10009        let mock_ctx = ExecutorContext::new_mock(None).await;
10010        let version = Version(0);
10011
10012        frontend.program = program.clone();
10013        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10014        frontend.update_state_after_exec(outcome, true);
10015        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10016        let sketch_id = sketch_object.id;
10017        let sketch = expect_sketch(sketch_object);
10018        let line1_id = *sketch.segments.get(3).unwrap();
10019
10020        let (src_delta, scene_delta) = frontend
10021            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10022            .await
10023            .unwrap();
10024        insta::assert_snapshot!(
10025            "test_delete_line_cascades_to_midpoint_constraint",
10026            src_delta.text.as_str()
10027        );
10028        assert_eq!(
10029            scene_delta.new_graph.objects.len(),
10030            3,
10031            "{:#?}",
10032            scene_delta.new_graph.objects
10033        );
10034
10035        mock_ctx.close().await;
10036    }
10037
10038    #[tokio::test(flavor = "multi_thread")]
10039    async fn test_delete_point_preserves_multiline_coincident_constraint() {
10040        let initial_source = "\
10041sketch(on = XY) {
10042  point1 = point(at = [var 1, var 2])
10043  point2 = point(at = [var 3, var 4])
10044  point3 = point(at = [var 5, var 6])
10045  coincident([point1, point2, point3])
10046}
10047";
10048
10049        let program = Program::parse(initial_source).unwrap().0.unwrap();
10050
10051        let mut frontend = FrontendState::new();
10052
10053        let mock_ctx = ExecutorContext::new_mock(None).await;
10054        let version = Version(0);
10055
10056        frontend.program = program.clone();
10057        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10058        frontend.update_state_after_exec(outcome, true);
10059        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10060        let sketch_id = sketch_object.id;
10061        let sketch = expect_sketch(sketch_object);
10062        let point3_id = *sketch.segments.get(2).unwrap();
10063
10064        let (src_delta, scene_delta) = frontend
10065            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10066            .await
10067            .unwrap();
10068        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10069        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10070        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10071        assert!(
10072            src_delta.text.contains("coincident([point1, point2])"),
10073            "{}",
10074            src_delta.text
10075        );
10076
10077        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10078        let sketch = expect_sketch(sketch_object);
10079        assert_eq!(sketch.segments.len(), 2);
10080        assert_eq!(sketch.constraints.len(), 1);
10081
10082        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10083        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10084            panic!("Expected constraint object");
10085        };
10086        let Constraint::Coincident(coincident) = constraint else {
10087            panic!("Expected coincident constraint");
10088        };
10089        assert_eq!(
10090            coincident.segments,
10091            sketch
10092                .segments
10093                .iter()
10094                .copied()
10095                .map(Into::into)
10096                .collect::<Vec<ConstraintSegment>>()
10097        );
10098
10099        mock_ctx.close().await;
10100    }
10101
10102    #[tokio::test(flavor = "multi_thread")]
10103    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10104        let initial_source = "\
10105sketch(on = XY) {
10106  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10107  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10108  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10109  equalLength([line1, line2, line3])
10110}
10111";
10112
10113        let program = Program::parse(initial_source).unwrap().0.unwrap();
10114
10115        let mut frontend = FrontendState::new();
10116
10117        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10118        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10119        let version = Version(0);
10120
10121        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10122        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10123        let sketch_id = sketch_object.id;
10124        let sketch = expect_sketch(sketch_object);
10125        let line3_id = *sketch.segments.get(8).unwrap();
10126
10127        let (src_delta, scene_delta) = frontend
10128            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10129            .await
10130            .unwrap();
10131        insta::assert_snapshot!(
10132            "test_delete_line_preserves_multiline_equal_length_constraint",
10133            src_delta.text.as_str()
10134        );
10135
10136        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10137        let sketch = expect_sketch(sketch_object);
10138        assert_eq!(sketch.constraints.len(), 1);
10139
10140        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10141        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10142            panic!("Expected constraint object");
10143        };
10144        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10145            panic!("Expected lines equal length constraint");
10146        };
10147        assert_eq!(lines_equal_length.lines.len(), 2);
10148
10149        ctx.close().await;
10150        mock_ctx.close().await;
10151    }
10152
10153    #[tokio::test(flavor = "multi_thread")]
10154    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10155        let initial_source = "\
10156sketch(on = XY) {
10157  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10158  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10159  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10160  horizontal([line1.end, line2.start, line3.start])
10161}
10162";
10163
10164        let program = Program::parse(initial_source).unwrap().0.unwrap();
10165
10166        let mut frontend = FrontendState::new();
10167
10168        let mock_ctx = ExecutorContext::new_mock(None).await;
10169        let version = Version(0);
10170
10171        frontend.program = program.clone();
10172        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10173        frontend.update_state_after_exec(outcome, true);
10174        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10175        let sketch_id = sketch_object.id;
10176        let sketch = expect_sketch(sketch_object);
10177        let line1_id = *sketch.segments.get(2).unwrap();
10178
10179        let (src_delta, scene_delta) = frontend
10180            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10181            .await
10182            .unwrap();
10183        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10184        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10185        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10186        assert!(
10187            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10188            "{}",
10189            src_delta.text
10190        );
10191
10192        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10193        let sketch = expect_sketch(sketch_object);
10194        assert_eq!(sketch.constraints.len(), 1);
10195
10196        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10197        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10198            panic!("Expected constraint object");
10199        };
10200        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10201            panic!("Expected horizontal points constraint");
10202        };
10203        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10204        assert_eq!(*points, remaining_points);
10205
10206        mock_ctx.close().await;
10207    }
10208
10209    #[tokio::test(flavor = "multi_thread")]
10210    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10211        let initial_source = "\
10212sketch(on = XY) {
10213  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10214  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10215  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10216  vertical([line1.end, line2.start, line3.start])
10217}
10218";
10219
10220        let program = Program::parse(initial_source).unwrap().0.unwrap();
10221
10222        let mut frontend = FrontendState::new();
10223
10224        let mock_ctx = ExecutorContext::new_mock(None).await;
10225        let version = Version(0);
10226
10227        frontend.program = program.clone();
10228        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10229        frontend.update_state_after_exec(outcome, true);
10230        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10231        let sketch_id = sketch_object.id;
10232        let sketch = expect_sketch(sketch_object);
10233        let line1_id = *sketch.segments.get(2).unwrap();
10234
10235        let (src_delta, scene_delta) = frontend
10236            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10237            .await
10238            .unwrap();
10239        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10240        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10241        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10242        assert!(
10243            src_delta.text.contains("vertical([line2.start, line3.start])"),
10244            "{}",
10245            src_delta.text
10246        );
10247
10248        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10249        let sketch = expect_sketch(sketch_object);
10250        assert_eq!(sketch.constraints.len(), 1);
10251
10252        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10253        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10254            panic!("Expected constraint object");
10255        };
10256        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10257            panic!("Expected vertical points constraint");
10258        };
10259        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10260        assert_eq!(*points, remaining_points);
10261
10262        mock_ctx.close().await;
10263    }
10264
10265    #[tokio::test(flavor = "multi_thread")]
10266    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10267        let initial_source = "\
10268sketch(on = XY) {
10269  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10270  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10271  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10272  coincident([line1.end, line2.start, line3.start])
10273}
10274";
10275
10276        let program = Program::parse(initial_source).unwrap().0.unwrap();
10277
10278        let mut frontend = FrontendState::new();
10279
10280        let mock_ctx = ExecutorContext::new_mock(None).await;
10281        let version = Version(0);
10282
10283        frontend.program = program.clone();
10284        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10285        frontend.update_state_after_exec(outcome, true);
10286        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10287        let sketch_id = sketch_object.id;
10288        let sketch = expect_sketch(sketch_object);
10289        let line1_id = *sketch.segments.get(2).unwrap();
10290
10291        let (src_delta, scene_delta) = frontend
10292            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10293            .await
10294            .unwrap();
10295        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10296        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10297        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10298        assert!(
10299            src_delta.text.contains("coincident([line2.start, line3.start])"),
10300            "{}",
10301            src_delta.text
10302        );
10303
10304        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10305        let sketch = expect_sketch(sketch_object);
10306        assert_eq!(sketch.constraints.len(), 1);
10307
10308        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10309        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10310            panic!("Expected constraint object");
10311        };
10312        let Constraint::Coincident(coincident) = constraint else {
10313            panic!("Expected coincident constraint");
10314        };
10315        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10316        assert_eq!(coincident.segments, remaining_segments);
10317
10318        mock_ctx.close().await;
10319    }
10320
10321    #[tokio::test(flavor = "multi_thread")]
10322    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10323        let initial_source = "\
10324sketch(on = XY) {
10325  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10326  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10327  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10328  equalLength([line1, line2, line3])
10329}
10330";
10331
10332        let program = Program::parse(initial_source).unwrap().0.unwrap();
10333
10334        let mut frontend = FrontendState::new();
10335
10336        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10337        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10338        let version = Version(0);
10339
10340        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10341        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10342        let sketch_id = sketch_object.id;
10343        let sketch = expect_sketch(sketch_object);
10344        let line2_id = *sketch.segments.get(5).unwrap();
10345        let line3_id = *sketch.segments.get(8).unwrap();
10346
10347        let (src_delta, scene_delta) = frontend
10348            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10349            .await
10350            .unwrap();
10351        insta::assert_snapshot!(
10352            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10353            src_delta.text.as_str()
10354        );
10355
10356        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10357        let sketch = expect_sketch(sketch_object);
10358        assert!(sketch.constraints.is_empty());
10359
10360        ctx.close().await;
10361        mock_ctx.close().await;
10362    }
10363
10364    #[tokio::test(flavor = "multi_thread")]
10365    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10366        let initial_source = "\
10367sketch(on = XY) {
10368  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10369  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10370  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10371  parallel([line1, line2, line3])
10372}
10373";
10374
10375        let program = Program::parse(initial_source).unwrap().0.unwrap();
10376
10377        let mut frontend = FrontendState::new();
10378
10379        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10380        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10381        let version = Version(0);
10382
10383        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10384        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10385        let sketch_id = sketch_object.id;
10386        let sketch = expect_sketch(sketch_object);
10387        let line3_id = *sketch.segments.get(8).unwrap();
10388
10389        let (src_delta, scene_delta) = frontend
10390            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10391            .await
10392            .unwrap();
10393        insta::assert_snapshot!(
10394            "test_delete_line_preserves_multiline_parallel_constraint",
10395            src_delta.text.as_str()
10396        );
10397
10398        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10399        let sketch = expect_sketch(sketch_object);
10400        assert_eq!(sketch.constraints.len(), 1);
10401
10402        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10403        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10404            panic!("Expected constraint object");
10405        };
10406        let Constraint::Parallel(parallel) = constraint else {
10407            panic!("Expected parallel constraint");
10408        };
10409        assert_eq!(parallel.lines.len(), 2);
10410
10411        ctx.close().await;
10412        mock_ctx.close().await;
10413    }
10414
10415    #[tokio::test(flavor = "multi_thread")]
10416    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10417        let initial_source = "\
10418sketch(on = XY) {
10419  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10420  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10421  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10422  parallel([line1, line2, line3])
10423}
10424";
10425
10426        let program = Program::parse(initial_source).unwrap().0.unwrap();
10427
10428        let mut frontend = FrontendState::new();
10429
10430        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10431        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10432        let version = Version(0);
10433
10434        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10435        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10436        let sketch_id = sketch_object.id;
10437        let sketch = expect_sketch(sketch_object);
10438        let line2_id = *sketch.segments.get(5).unwrap();
10439        let line3_id = *sketch.segments.get(8).unwrap();
10440
10441        let (src_delta, scene_delta) = frontend
10442            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10443            .await
10444            .unwrap();
10445        insta::assert_snapshot!(
10446            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10447            src_delta.text.as_str()
10448        );
10449
10450        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10451        let sketch = expect_sketch(sketch_object);
10452        assert!(sketch.constraints.is_empty());
10453
10454        ctx.close().await;
10455        mock_ctx.close().await;
10456    }
10457
10458    #[tokio::test(flavor = "multi_thread")]
10459    async fn test_delete_line_line_coincident_constraint() {
10460        let initial_source = "\
10461sketch(on = XY) {
10462  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10463  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10464  coincident([line1, line2])
10465}
10466";
10467
10468        let program = Program::parse(initial_source).unwrap().0.unwrap();
10469
10470        let mut frontend = FrontendState::new();
10471
10472        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10473        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10474        let version = Version(0);
10475
10476        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10477        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10478        let sketch_id = sketch_object.id;
10479        let sketch = expect_sketch(sketch_object);
10480
10481        let coincident_id = *sketch.constraints.first().unwrap();
10482
10483        let (src_delta, scene_delta) = frontend
10484            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10485            .await
10486            .unwrap();
10487        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10488        assert_eq!(scene_delta.new_objects, vec![]);
10489        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10490
10491        ctx.close().await;
10492        mock_ctx.close().await;
10493    }
10494
10495    #[tokio::test(flavor = "multi_thread")]
10496    async fn test_two_points_coincident() {
10497        let initial_source = "\
10498sketch(on = XY) {
10499  point1 = point(at = [var 1, var 2])
10500  point(at = [3, 4])
10501}
10502";
10503
10504        let program = Program::parse(initial_source).unwrap().0.unwrap();
10505
10506        let mut frontend = FrontendState::new();
10507
10508        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10509        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10510        let version = Version(0);
10511
10512        frontend.hack_set_program(&ctx, program).await.unwrap();
10513        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10514        let sketch_id = sketch_object.id;
10515        let sketch = expect_sketch(sketch_object);
10516        let point0_id = *sketch.segments.first().unwrap();
10517        let point1_id = *sketch.segments.get(1).unwrap();
10518
10519        let constraint = Constraint::Coincident(Coincident {
10520            segments: vec![point0_id.into(), point1_id.into()],
10521        });
10522        let (src_delta, scene_delta) = frontend
10523            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10524            .await
10525            .unwrap();
10526        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10527        assert_eq!(
10528            scene_delta.new_graph.objects.len(),
10529            5,
10530            "{:#?}",
10531            scene_delta.new_graph.objects
10532        );
10533
10534        ctx.close().await;
10535        mock_ctx.close().await;
10536    }
10537
10538    #[tokio::test(flavor = "multi_thread")]
10539    async fn test_three_points_coincident() {
10540        let initial_source = "\
10541sketch(on = XY) {
10542  point1 = point(at = [var 1, var 2])
10543  point(at = [var 3, var 4])
10544  point(at = [var 5, var 6])
10545}
10546";
10547
10548        let program = Program::parse(initial_source).unwrap().0.unwrap();
10549
10550        let mut frontend = FrontendState::new();
10551
10552        let mock_ctx = ExecutorContext::new_mock(None).await;
10553        let version = Version(0);
10554
10555        frontend.program = program.clone();
10556        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10557        frontend.update_state_after_exec(outcome, true);
10558        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10559        let sketch_id = sketch_object.id;
10560        let sketch = expect_sketch(sketch_object);
10561        let segments = sketch
10562            .segments
10563            .iter()
10564            .take(3)
10565            .copied()
10566            .map(Into::into)
10567            .collect::<Vec<ConstraintSegment>>();
10568
10569        let constraint = Constraint::Coincident(Coincident {
10570            segments: segments.clone(),
10571        });
10572        let (src_delta, scene_delta) = frontend
10573            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10574            .await
10575            .unwrap();
10576        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10577
10578        let constraint_object = scene_delta
10579            .new_graph
10580            .objects
10581            .iter()
10582            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10583            .unwrap();
10584
10585        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10586            panic!("expected a constraint object");
10587        };
10588
10589        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10590
10591        mock_ctx.close().await;
10592    }
10593
10594    #[tokio::test(flavor = "multi_thread")]
10595    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10596        let initial_source = "\
10597sketch(on = XY) {
10598  point1 = point(at = [var 1, var 2])
10599  point2 = point(at = [var 3, var 4])
10600  point3 = point(at = [var 5, var 6])
10601  coincident([point1, point2, point3])
10602}
10603";
10604
10605        let program = Program::parse(initial_source).unwrap().0.unwrap();
10606
10607        let mut frontend = FrontendState::new();
10608
10609        let ctx = ExecutorContext::new_mock(None).await;
10610        frontend.program = program.clone();
10611        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10612        frontend.update_state_after_exec(outcome, true);
10613
10614        let constraint_object = frontend
10615            .scene_graph
10616            .objects
10617            .iter()
10618            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10619            .unwrap();
10620        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10621            panic!("expected a constraint object");
10622        };
10623
10624        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625        let sketch = expect_sketch(sketch_object);
10626        let expected_segments = sketch
10627            .segments
10628            .iter()
10629            .take(3)
10630            .copied()
10631            .map(Into::into)
10632            .collect::<Vec<ConstraintSegment>>();
10633
10634        assert_eq!(
10635            constraint,
10636            &Constraint::Coincident(Coincident {
10637                segments: expected_segments,
10638            })
10639        );
10640
10641        ctx.close().await;
10642    }
10643
10644    #[tokio::test(flavor = "multi_thread")]
10645    async fn test_point_origin_coincident_preserves_order() {
10646        let initial_source = "\
10647sketch(on = XY) {
10648  point(at = [var 1, var 2])
10649}
10650";
10651
10652        for (origin_first, snapshot_name) in [
10653            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10654            (false, "test_point_origin_coincident_preserves_order_point_first"),
10655        ] {
10656            let program = Program::parse(initial_source).unwrap().0.unwrap();
10657
10658            let mut frontend = FrontendState::new();
10659
10660            let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10661            let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10662            let version = Version(0);
10663
10664            frontend.hack_set_program(&ctx, program).await.unwrap();
10665            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10666            let sketch_id = sketch_object.id;
10667            let sketch = expect_sketch(sketch_object);
10668            let point_id = *sketch.segments.first().unwrap();
10669
10670            let segments = if origin_first {
10671                vec![ConstraintSegment::ORIGIN, point_id.into()]
10672            } else {
10673                vec![point_id.into(), ConstraintSegment::ORIGIN]
10674            };
10675            let constraint = Constraint::Coincident(Coincident {
10676                segments: segments.clone(),
10677            });
10678            let (src_delta, scene_delta) = frontend
10679                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10680                .await
10681                .unwrap();
10682            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10683
10684            let constraint_object = scene_delta
10685                .new_graph
10686                .objects
10687                .iter()
10688                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10689                .unwrap();
10690
10691            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10692                panic!("expected a constraint object");
10693            };
10694
10695            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10696
10697            ctx.close().await;
10698            mock_ctx.close().await;
10699        }
10700    }
10701
10702    #[tokio::test(flavor = "multi_thread")]
10703    async fn test_coincident_of_line_end_points() {
10704        let initial_source = "\
10705sketch(on = XY) {
10706  line(start = [var 1, var 2], end = [var 3, var 4])
10707  line(start = [var 5, var 6], end = [var 7, var 8])
10708}
10709";
10710
10711        let program = Program::parse(initial_source).unwrap().0.unwrap();
10712
10713        let mut frontend = FrontendState::new();
10714
10715        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10716        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10717        let version = Version(0);
10718
10719        frontend.hack_set_program(&ctx, program).await.unwrap();
10720        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10721        let sketch_id = sketch_object.id;
10722        let sketch = expect_sketch(sketch_object);
10723        let point0_id = *sketch.segments.get(1).unwrap();
10724        let point1_id = *sketch.segments.get(3).unwrap();
10725
10726        let constraint = Constraint::Coincident(Coincident {
10727            segments: vec![point0_id.into(), point1_id.into()],
10728        });
10729        let (src_delta, scene_delta) = frontend
10730            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10731            .await
10732            .unwrap();
10733        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10734        assert_eq!(
10735            scene_delta.new_graph.objects.len(),
10736            9,
10737            "{:#?}",
10738            scene_delta.new_graph.objects
10739        );
10740
10741        ctx.close().await;
10742        mock_ctx.close().await;
10743    }
10744
10745    #[tokio::test(flavor = "multi_thread")]
10746    async fn test_coincident_of_line_point_and_circle_segment() {
10747        let initial_source = "\
10748sketch(on = XY) {
10749  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10750  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10751}
10752";
10753        let program = Program::parse(initial_source).unwrap().0.unwrap();
10754        let mut frontend = FrontendState::new();
10755
10756        let mock_ctx = ExecutorContext::new_mock(None).await;
10757        let version = Version(0);
10758
10759        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10760        frontend.program = program;
10761        frontend.update_state_after_exec(outcome, true);
10762        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10763        let sketch_id = sketch_object.id;
10764        let sketch = expect_sketch(sketch_object);
10765
10766        let circle_id = sketch
10767            .segments
10768            .iter()
10769            .copied()
10770            .find(|seg_id| {
10771                matches!(
10772                    &frontend.scene_graph.objects[seg_id.0].kind,
10773                    ObjectKind::Segment {
10774                        segment: Segment::Circle(_)
10775                    }
10776                )
10777            })
10778            .expect("Expected a circle segment in sketch");
10779        let line_id = frontend
10780            .scene_graph
10781            .objects
10782            .iter()
10783            .find_map(|obj| match &obj.kind {
10784                ObjectKind::Segment {
10785                    segment: Segment::Line(line),
10786                } if line.owner.is_none() => Some(obj.id),
10787                _ => None,
10788            })
10789            .expect("Expected a standalone line segment in scene graph");
10790
10791        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10792            ObjectKind::Segment {
10793                segment: Segment::Line(line),
10794            } => line.start,
10795            _ => panic!("Expected line segment object"),
10796        };
10797
10798        let constraint = Constraint::Coincident(Coincident {
10799            segments: vec![line_start_point_id.into(), circle_id.into()],
10800        });
10801        let (src_delta, _scene_delta) = frontend
10802            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10803            .await
10804            .unwrap();
10805        insta::assert_snapshot!(
10806            "test_coincident_of_line_point_and_circle_segment",
10807            src_delta.text.as_str()
10808        );
10809
10810        mock_ctx.close().await;
10811    }
10812
10813    #[tokio::test(flavor = "multi_thread")]
10814    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10815        // Test that attempting an invalid coincident constraint (arc and line)
10816        // doesn't corrupt the state, allowing subsequent operations to work.
10817        // This test verifies the transactional fix in add_constraint that prevents
10818        // state corruption when invalid constraints are attempted.
10819        // Example: coincident constraint between an arc segment and a straight line segment
10820        // is geometrically invalid and should fail, but state should remain intact.
10821        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10822        let program = Program::empty();
10823
10824        let mut frontend = FrontendState::new();
10825        frontend.program = program;
10826
10827        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10828        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10829        let version = Version(0);
10830
10831        let sketch_args = SketchCtor {
10832            on: Plane::Default(PlaneName::Xy),
10833        };
10834        let (_src_delta, _scene_delta, sketch_id) = frontend
10835            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10836            .await
10837            .unwrap();
10838
10839        // Add an arc segment
10840        let arc_ctor = ArcCtor {
10841            start: Point2d {
10842                x: Expr::Var(Number {
10843                    value: 0.0,
10844                    units: NumericSuffix::Mm,
10845                }),
10846                y: Expr::Var(Number {
10847                    value: 0.0,
10848                    units: NumericSuffix::Mm,
10849                }),
10850            },
10851            end: Point2d {
10852                x: Expr::Var(Number {
10853                    value: 10.0,
10854                    units: NumericSuffix::Mm,
10855                }),
10856                y: Expr::Var(Number {
10857                    value: 10.0,
10858                    units: NumericSuffix::Mm,
10859                }),
10860            },
10861            center: Point2d {
10862                x: Expr::Var(Number {
10863                    value: 10.0,
10864                    units: NumericSuffix::Mm,
10865                }),
10866                y: Expr::Var(Number {
10867                    value: 0.0,
10868                    units: NumericSuffix::Mm,
10869                }),
10870            },
10871            direction: None,
10872            construction: None,
10873        };
10874        let (_src_delta, scene_delta) = frontend
10875            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10876            .await
10877            .unwrap();
10878        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10879        let arc_id = *scene_delta.new_objects.last().unwrap();
10880
10881        // Add a line segment
10882        let line_ctor = LineCtor {
10883            start: Point2d {
10884                x: Expr::Var(Number {
10885                    value: 20.0,
10886                    units: NumericSuffix::Mm,
10887                }),
10888                y: Expr::Var(Number {
10889                    value: 0.0,
10890                    units: NumericSuffix::Mm,
10891                }),
10892            },
10893            end: Point2d {
10894                x: Expr::Var(Number {
10895                    value: 30.0,
10896                    units: NumericSuffix::Mm,
10897                }),
10898                y: Expr::Var(Number {
10899                    value: 10.0,
10900                    units: NumericSuffix::Mm,
10901                }),
10902            },
10903            construction: None,
10904        };
10905        let (_src_delta, scene_delta) = frontend
10906            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10907            .await
10908            .unwrap();
10909        // The line is the last object in new_objects (after the 2 points: start, end)
10910        let line_id = *scene_delta.new_objects.last().unwrap();
10911
10912        // Attempt to add an invalid coincident constraint between arc and line
10913        // This should fail during execution, but state should remain intact
10914        let constraint = Constraint::Coincident(Coincident {
10915            segments: vec![arc_id.into(), line_id.into()],
10916        });
10917        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10918
10919        // The constraint addition should fail (invalid constraint)
10920        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10921
10922        // Verify state is not corrupted by checking that we can still access the scene graph
10923        // and that the original segments are still present with their source ranges
10924        let sketch_object_after =
10925            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10926        let sketch_after = expect_sketch(sketch_object_after);
10927
10928        // Verify both segments are still in the sketch
10929        assert!(
10930            sketch_after.segments.contains(&arc_id),
10931            "Arc segment should still exist after failed constraint"
10932        );
10933        assert!(
10934            sketch_after.segments.contains(&line_id),
10935            "Line segment should still exist after failed constraint"
10936        );
10937
10938        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10939        let arc_obj = frontend
10940            .scene_graph
10941            .objects
10942            .get(arc_id.0)
10943            .expect("Arc object should still be accessible");
10944        let line_obj = frontend
10945            .scene_graph
10946            .objects
10947            .get(line_id.0)
10948            .expect("Line object should still be accessible");
10949
10950        // Verify source ranges are still valid (not corrupted)
10951        // Just verify that the objects are still accessible and have the expected types
10952        match &arc_obj.kind {
10953            ObjectKind::Segment {
10954                segment: Segment::Arc(_),
10955            } => {}
10956            _ => panic!("Arc object should still be an arc segment"),
10957        }
10958        match &line_obj.kind {
10959            ObjectKind::Segment {
10960                segment: Segment::Line(_),
10961            } => {}
10962            _ => panic!("Line object should still be a line segment"),
10963        }
10964
10965        ctx.close().await;
10966        mock_ctx.close().await;
10967    }
10968
10969    #[tokio::test(flavor = "multi_thread")]
10970    async fn test_distance_two_points() {
10971        let initial_source = "\
10972sketch(on = XY) {
10973  point(at = [var 1, var 2])
10974  point(at = [var 3, var 4])
10975}
10976";
10977
10978        let program = Program::parse(initial_source).unwrap().0.unwrap();
10979
10980        let mut frontend = FrontendState::new();
10981
10982        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10983        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10984        let version = Version(0);
10985
10986        frontend.hack_set_program(&ctx, program).await.unwrap();
10987        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10988        let sketch_id = sketch_object.id;
10989        let sketch = expect_sketch(sketch_object);
10990        let point0_id = *sketch.segments.first().unwrap();
10991        let point1_id = *sketch.segments.get(1).unwrap();
10992
10993        let constraint = Constraint::Distance(Distance {
10994            segments: vec![point0_id.into(), point1_id.into()],
10995            distance: Number {
10996                value: 2.0,
10997                units: NumericSuffix::Mm,
10998            },
10999            label_position: None,
11000            source: Default::default(),
11001        });
11002        let (src_delta, scene_delta) = frontend
11003            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11004            .await
11005            .unwrap();
11006        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11007        assert_eq!(
11008            scene_delta.new_graph.objects.len(),
11009            5,
11010            "{:#?}",
11011            scene_delta.new_graph.objects
11012        );
11013
11014        ctx.close().await;
11015        mock_ctx.close().await;
11016    }
11017
11018    #[tokio::test(flavor = "multi_thread")]
11019    async fn test_distance_two_points_with_label() {
11020        let initial_source = "\
11021sketch(on = XY) {
11022  point(at = [var 1, var 2])
11023  point(at = [var 3, var 4])
11024}
11025";
11026
11027        let program = Program::parse(initial_source).unwrap().0.unwrap();
11028
11029        let mut frontend = FrontendState::new();
11030
11031        let mock_ctx = ExecutorContext::new_mock(None).await;
11032        let version = Version(0);
11033
11034        frontend.program = program.clone();
11035        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11036        frontend.update_state_after_exec(outcome, true);
11037        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11038        let sketch_id = sketch_object.id;
11039        let sketch = expect_sketch(sketch_object);
11040        let point0_id = *sketch.segments.first().unwrap();
11041        let point1_id = *sketch.segments.get(1).unwrap();
11042
11043        let label_position = Point2d {
11044            x: Number {
11045                value: 10.0,
11046                units: NumericSuffix::Mm,
11047            },
11048            y: Number {
11049                value: 11.0,
11050                units: NumericSuffix::Mm,
11051            },
11052        };
11053        let constraint = Constraint::Distance(Distance {
11054            segments: vec![point0_id.into(), point1_id.into()],
11055            distance: Number {
11056                value: 2.0,
11057                units: NumericSuffix::Mm,
11058            },
11059            label_position: Some(label_position.clone()),
11060            source: Default::default(),
11061        });
11062        let (src_delta, scene_delta) = frontend
11063            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11064            .await
11065            .unwrap();
11066        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11067
11068        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069        let sketch = expect_sketch(sketch_object);
11070        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11071        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11072            panic!("Expected constraint object");
11073        };
11074        let Constraint::Distance(distance) = constraint else {
11075            panic!("Expected distance constraint");
11076        };
11077        assert_eq!(distance.label_position, Some(label_position));
11078
11079        mock_ctx.close().await;
11080    }
11081
11082    #[tokio::test(flavor = "multi_thread")]
11083    async fn test_edit_distance_constraint_label_position() {
11084        let initial_source = "\
11085sketch(on = XY) {
11086  point(at = [var 1, var 2])
11087  point(at = [var 3, var 2])
11088}
11089";
11090
11091        let program = Program::parse(initial_source).unwrap().0.unwrap();
11092
11093        let mut frontend = FrontendState::new();
11094
11095        let mock_ctx = ExecutorContext::new_mock(None).await;
11096        let version = Version(0);
11097
11098        frontend.program = program.clone();
11099        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11100        frontend.update_state_after_exec(outcome, true);
11101        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11102        let sketch_id = sketch_object.id;
11103        let sketch = expect_sketch(sketch_object);
11104        let point0_id = *sketch.segments.first().unwrap();
11105        let point1_id = *sketch.segments.get(1).unwrap();
11106
11107        let constraint = Constraint::Distance(Distance {
11108            segments: vec![point0_id.into(), point1_id.into()],
11109            distance: Number {
11110                value: 2.0,
11111                units: NumericSuffix::Mm,
11112            },
11113            label_position: None,
11114            source: Default::default(),
11115        });
11116        let (_, scene_delta) = frontend
11117            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11118            .await
11119            .unwrap();
11120        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11121        let sketch = expect_sketch(sketch_object);
11122        let constraint_id = sketch.constraints[0];
11123        let label_position = Point2d {
11124            x: Number {
11125                value: 10.0,
11126                units: NumericSuffix::Mm,
11127            },
11128            y: Number {
11129                value: 11.0,
11130                units: NumericSuffix::Mm,
11131            },
11132        };
11133
11134        let (src_delta, scene_delta) = frontend
11135            .edit_distance_constraint_label_position(
11136                &mock_ctx,
11137                version,
11138                sketch_id,
11139                constraint_id,
11140                label_position.clone(),
11141                vec![],
11142            )
11143            .await
11144            .unwrap();
11145        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11146
11147        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11148        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11149            panic!("Expected constraint object");
11150        };
11151        let Constraint::Distance(distance) = constraint else {
11152            panic!("Expected distance constraint");
11153        };
11154        assert_eq!(distance.label_position, Some(label_position));
11155
11156        mock_ctx.close().await;
11157    }
11158
11159    #[tokio::test(flavor = "multi_thread")]
11160    async fn test_edit_distance_constraint_type_and_value() {
11161        let initial_source = "\
11162sketch(on = XY) {
11163  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11164  distance([line1.start, line1.end]) == 5mm
11165}
11166";
11167
11168        let program = Program::parse(initial_source).unwrap().0.unwrap();
11169        let mut frontend = FrontendState::new();
11170        let mock_ctx = ExecutorContext::new_mock(None).await;
11171        let version = Version(0);
11172
11173        frontend.program = program.clone();
11174        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11175        frontend.update_state_after_exec(outcome, true);
11176        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11177        let sketch_id = sketch_object.id;
11178        let sketch = expect_sketch(sketch_object);
11179        let constraint_id = sketch.constraints[0];
11180        let point0_id = sketch.segments[0];
11181        let point1_id = sketch.segments[1];
11182        let label_position = Point2d {
11183            x: Number {
11184                value: 2.0,
11185                units: NumericSuffix::Mm,
11186            },
11187            y: Number {
11188                value: 5.0,
11189                units: NumericSuffix::Mm,
11190            },
11191        };
11192
11193        let (source_delta, scene_delta) = frontend
11194            .edit_distance_constraint_with_options(
11195                &mock_ctx,
11196                version,
11197                sketch_id,
11198                constraint_id,
11199                Constraint::HorizontalDistance(Distance {
11200                    segments: vec![point0_id.into(), point1_id.into()],
11201                    distance: Number {
11202                        value: 4.0,
11203                        units: NumericSuffix::Mm,
11204                    },
11205                    label_position: Some(label_position.clone()),
11206                    source: Default::default(),
11207                }),
11208                EditConstraintOptions {
11209                    commit_solved_initial_guesses: false,
11210                },
11211            )
11212            .await
11213            .unwrap();
11214        assert_eq!(
11215            source_delta.text,
11216            "\
11217sketch(on = XY) {
11218  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11219  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11220}
11221"
11222        );
11223
11224        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11225        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11226            panic!("Expected constraint object");
11227        };
11228        let Constraint::HorizontalDistance(distance) = constraint else {
11229            panic!("Expected horizontal distance constraint");
11230        };
11231        assert_eq!(distance.distance.value, 4.0);
11232        assert_eq!(distance.label_position, Some(label_position));
11233
11234        mock_ctx.close().await;
11235    }
11236
11237    #[tokio::test(flavor = "multi_thread")]
11238    async fn test_edit_angle_constraint_label_position() {
11239        let initial_source = "\
11240sketch(on = XY) {
11241  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11242  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11243  angle([line1, line2]) == 60deg
11244}
11245";
11246
11247        let program = Program::parse(initial_source).unwrap().0.unwrap();
11248        let mut frontend = FrontendState::new();
11249        let mock_ctx = ExecutorContext::new_mock(None).await;
11250        let version = Version(0);
11251
11252        frontend.program = program.clone();
11253        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11254        frontend.update_state_after_exec(outcome, true);
11255        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11256        let sketch_id = sketch_object.id;
11257        let sketch = expect_sketch(sketch_object);
11258        let constraint_id = sketch.constraints[0];
11259        let label_position = Point2d {
11260            x: Number {
11261                value: 10.0,
11262                units: NumericSuffix::Mm,
11263            },
11264            y: Number {
11265                value: 11.0,
11266                units: NumericSuffix::Mm,
11267            },
11268        };
11269
11270        let (src_delta, scene_delta) = frontend
11271            .edit_distance_constraint_label_position(
11272                &mock_ctx,
11273                version,
11274                sketch_id,
11275                constraint_id,
11276                label_position.clone(),
11277                vec![],
11278            )
11279            .await
11280            .unwrap();
11281        assert_eq!(
11282            src_delta.text.as_str(),
11283            "\
11284sketch(on = XY) {
11285  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11286  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11287  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11288}
11289"
11290        );
11291
11292        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11293        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11294            panic!("Expected constraint object");
11295        };
11296        let Constraint::Angle(angle) = constraint else {
11297            panic!("Expected angle constraint");
11298        };
11299        assert_eq!(angle.label_position, Some(label_position));
11300
11301        mock_ctx.close().await;
11302    }
11303
11304    #[tokio::test(flavor = "multi_thread")]
11305    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11306        let initial_source = "\
11307sketch(on = XY) {
11308  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11309  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11310  60deg == angleDimension(lines = [line1, line2], sector = 1)
11311}
11312";
11313
11314        let program = Program::parse(initial_source).unwrap().0.unwrap();
11315        let mut frontend = FrontendState::new();
11316        let mock_ctx = ExecutorContext::new_mock(None).await;
11317        let version = Version(0);
11318
11319        frontend.program = program.clone();
11320        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11321        frontend.update_state_after_exec(outcome, true);
11322        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11323        let sketch_id = sketch_object.id;
11324        let sketch = expect_sketch(sketch_object);
11325        let constraint_id = sketch.constraints[0];
11326        let label_position = Point2d {
11327            x: Number {
11328                value: 10.0,
11329                units: NumericSuffix::Mm,
11330            },
11331            y: Number {
11332                value: 11.0,
11333                units: NumericSuffix::Mm,
11334            },
11335        };
11336
11337        let (src_delta, scene_delta) = frontend
11338            .edit_distance_constraint_label_position(
11339                &mock_ctx,
11340                version,
11341                sketch_id,
11342                constraint_id,
11343                label_position.clone(),
11344                vec![],
11345            )
11346            .await
11347            .unwrap();
11348        assert_eq!(
11349            src_delta.text.as_str(),
11350            "\
11351sketch(on = XY) {
11352  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11353  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11354  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11355}
11356"
11357        );
11358
11359        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11360        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11361            panic!("Expected constraint object");
11362        };
11363        let Constraint::Angle(angle) = constraint else {
11364            panic!("Expected angle constraint");
11365        };
11366        assert_eq!(angle.label_position, Some(label_position));
11367
11368        mock_ctx.close().await;
11369    }
11370
11371    #[tokio::test(flavor = "multi_thread")]
11372    async fn test_edit_angle_constraint() {
11373        let initial_source = "\
11374sketch(on = XY) {
11375  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11376  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11377  angle([line1, line2]) == 60deg
11378}
11379";
11380
11381        let program = Program::parse(initial_source).unwrap().0.unwrap();
11382        let mut frontend = FrontendState::new();
11383        let mock_ctx = ExecutorContext::new_mock(None).await;
11384        let version = Version(0);
11385
11386        frontend.program = program.clone();
11387        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11388        frontend.update_state_after_exec(outcome, true);
11389        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11390        let sketch_id = sketch_object.id;
11391        let sketch = expect_sketch(sketch_object);
11392        let constraint_id = sketch.constraints[0];
11393        let line1_id = *sketch.segments.get(2).unwrap();
11394        let line2_id = *sketch.segments.get(5).unwrap();
11395        let label_position = Point2d {
11396            x: Number {
11397                value: 10.0,
11398                units: NumericSuffix::Mm,
11399            },
11400            y: Number {
11401                value: 11.0,
11402                units: NumericSuffix::Mm,
11403            },
11404        };
11405
11406        let (src_delta, scene_delta) = frontend
11407            .edit_angle_constraint_with_options(
11408                &mock_ctx,
11409                version,
11410                sketch_id,
11411                constraint_id,
11412                Angle {
11413                    lines: vec![line2_id, line1_id],
11414                    angle: Number {
11415                        value: 60.0,
11416                        units: NumericSuffix::Deg,
11417                    },
11418                    sector: Some(3),
11419                    inverse: Some(false),
11420                    label_position: Some(label_position.clone()),
11421                    source: Default::default(),
11422                },
11423                EditConstraintOptions {
11424                    commit_solved_initial_guesses: false,
11425                },
11426            )
11427            .await
11428            .unwrap();
11429        assert_eq!(
11430            src_delta.text.as_str(),
11431            "\
11432sketch(on = XY) {
11433  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11434  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11435  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11436}
11437"
11438        );
11439
11440        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11441        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11442            panic!("Expected constraint object");
11443        };
11444        let Constraint::Angle(angle) = constraint else {
11445            panic!("Expected angle constraint");
11446        };
11447        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11448        assert_eq!(angle.sector, Some(3));
11449        assert_eq!(angle.inverse, Some(false));
11450        assert_eq!(angle.label_position, Some(label_position));
11451
11452        mock_ctx.close().await;
11453    }
11454
11455    #[tokio::test(flavor = "multi_thread")]
11456    async fn test_edit_angle_constraint_with_call_on_right() {
11457        let initial_source = "\
11458sketch(on = XY) {
11459  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11460  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11461  60deg == angle([line1, line2])
11462}
11463";
11464
11465        let program = Program::parse(initial_source).unwrap().0.unwrap();
11466        let mut frontend = FrontendState::new();
11467        let mock_ctx = ExecutorContext::new_mock(None).await;
11468        let version = Version(0);
11469
11470        frontend.program = program.clone();
11471        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11472        frontend.update_state_after_exec(outcome, true);
11473        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11474        let sketch_id = sketch_object.id;
11475        let sketch = expect_sketch(sketch_object);
11476        let constraint_id = sketch.constraints[0];
11477        let line1_id = *sketch.segments.get(2).unwrap();
11478        let line2_id = *sketch.segments.get(5).unwrap();
11479
11480        let (src_delta, _) = frontend
11481            .edit_angle_constraint_with_options(
11482                &mock_ctx,
11483                version,
11484                sketch_id,
11485                constraint_id,
11486                Angle {
11487                    lines: vec![line2_id, line1_id],
11488                    angle: Number {
11489                        value: 60.0,
11490                        units: NumericSuffix::Deg,
11491                    },
11492                    sector: Some(3),
11493                    inverse: Some(false),
11494                    label_position: None,
11495                    source: Default::default(),
11496                },
11497                EditConstraintOptions {
11498                    commit_solved_initial_guesses: false,
11499                },
11500            )
11501            .await
11502            .unwrap();
11503        assert_eq!(
11504            src_delta.text.as_str(),
11505            "\
11506sketch(on = XY) {
11507  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11508  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11509  60deg == angleDimension(lines = [line2, line1], sector = 3)
11510}
11511"
11512        );
11513
11514        mock_ctx.close().await;
11515    }
11516
11517    #[tokio::test(flavor = "multi_thread")]
11518    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11519        let initial_source = "\
11520@settings(kclVersion = 2.0)
11521
11522sketch001 = sketch(on = XZ) {
11523  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11524  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11525  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11526  vertical([line1.start, ORIGIN])
11527}
11528";
11529
11530        let program = Program::parse(initial_source).unwrap().0.unwrap();
11531        let mut frontend = FrontendState::new();
11532        let mock_ctx = ExecutorContext::new_mock(None).await;
11533        let version = Version(0);
11534
11535        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11536        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11537        let sketch_id = sketch_object.id;
11538        let sketch = expect_sketch(sketch_object);
11539        let constraint_id = sketch
11540            .constraints
11541            .iter()
11542            .copied()
11543            .find(|constraint_id| {
11544                matches!(
11545                    frontend.scene_graph.objects[constraint_id.0].kind,
11546                    ObjectKind::Constraint {
11547                        constraint: Constraint::Distance(_)
11548                    }
11549                )
11550            })
11551            .unwrap();
11552        let line1_id = sketch
11553            .segments
11554            .iter()
11555            .copied()
11556            .find(|segment_id| {
11557                matches!(
11558                    frontend.scene_graph.objects[segment_id.0].kind,
11559                    ObjectKind::Segment {
11560                        segment: Segment::Line(_)
11561                    }
11562                )
11563            })
11564            .unwrap();
11565        let label_position = Point2d {
11566            x: Number {
11567                value: 7.0,
11568                units: NumericSuffix::Mm,
11569            },
11570            y: Number {
11571                value: 9.0,
11572                units: NumericSuffix::Mm,
11573            },
11574        };
11575
11576        let (source_delta, scene_delta) = frontend
11577            .edit_segments_with_options(
11578                &mock_ctx,
11579                version,
11580                sketch_id,
11581                vec![ExistingSegmentCtor {
11582                    id: line1_id,
11583                    ctor: SegmentCtor::Line(LineCtor {
11584                        start: point_expr_mm(2.0, 15.55),
11585                        end: point_expr_mm(-4.03, 11.51),
11586                        construction: None,
11587                    }),
11588                }],
11589                EditSegmentsOptions {
11590                    anchor_segment_ids: Some(vec![]),
11591                    drag_anchors: vec![SegmentDragAnchor {
11592                        segment_id: line1_id,
11593                        target: label_position.clone(),
11594                    }],
11595                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11596                        constraint_id,
11597                        label_position: label_position.clone(),
11598                    }],
11599                    commit_solved_initial_guesses: true,
11600                },
11601            )
11602            .await
11603            .unwrap();
11604
11605        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11606        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11607        let ObjectKind::Constraint {
11608            constraint: Constraint::Distance(distance),
11609        } = &constraint_object.kind
11610        else {
11611            panic!("Expected distance constraint object");
11612        };
11613        assert_eq!(distance.label_position, Some(label_position));
11614
11615        let snapped_label_position = Point2d {
11616            x: Number {
11617                value: 8.0,
11618                units: NumericSuffix::Mm,
11619            },
11620            y: Number {
11621                value: 10.0,
11622                units: NumericSuffix::Mm,
11623            },
11624        };
11625        let (source_delta, scene_delta) = frontend
11626            .edit_segments_with_options(
11627                &mock_ctx,
11628                version,
11629                sketch_id,
11630                vec![],
11631                EditSegmentsOptions {
11632                    anchor_segment_ids: Some(vec![line1_id]),
11633                    drag_anchors: vec![],
11634                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11635                        constraint_id,
11636                        label_position: snapped_label_position.clone(),
11637                    }],
11638                    commit_solved_initial_guesses: true,
11639                },
11640            )
11641            .await
11642            .unwrap();
11643
11644        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11645        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11646        let ObjectKind::Constraint {
11647            constraint: Constraint::Distance(distance),
11648        } = &constraint_object.kind
11649        else {
11650            panic!("Expected distance constraint object");
11651        };
11652        assert_eq!(distance.label_position, Some(snapped_label_position));
11653
11654        mock_ctx.close().await;
11655    }
11656
11657    #[tokio::test(flavor = "multi_thread")]
11658    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11659        let initial_source = "\
11660sketch(on = XY) {
11661  point1 = point(at = [var 0mm, var 0mm])
11662  point2 = point(at = [var 10mm, var 0mm])
11663  distance([point1, point2]) == 5mm
11664}
11665";
11666
11667        let program = Program::parse(initial_source).unwrap().0.unwrap();
11668        let mut frontend = FrontendState::new();
11669        let mock_ctx = ExecutorContext::new_mock(None).await;
11670        let version = Version(0);
11671
11672        frontend.program = program.clone();
11673        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11674        frontend.update_state_after_exec(outcome, true);
11675        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11676        let sketch_id = sketch_object.id;
11677        let sketch = expect_sketch(sketch_object);
11678        let point0_id = sketch.segments[0];
11679        let point1_id = sketch.segments[1];
11680        let constraint_id = sketch.constraints[0];
11681
11682        let edited_segments = vec![ExistingSegmentCtor {
11683            id: point0_id,
11684            ctor: SegmentCtor::Point(PointCtor {
11685                position: Point2d {
11686                    x: Expr::Var(Number {
11687                        value: 2.0,
11688                        units: NumericSuffix::Mm,
11689                    }),
11690                    y: Expr::Var(Number {
11691                        value: 1.0,
11692                        units: NumericSuffix::Mm,
11693                    }),
11694                },
11695            }),
11696        }];
11697        let (_, scene_delta) = frontend
11698            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11699            .await
11700            .unwrap();
11701        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11702        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11703
11704        let label_position = Point2d {
11705            x: Number {
11706                value: 3.0,
11707                units: NumericSuffix::Mm,
11708            },
11709            y: Number {
11710                value: 4.0,
11711                units: NumericSuffix::Mm,
11712            },
11713        };
11714        let (_, scene_delta) = frontend
11715            .edit_distance_constraint_label_position(
11716                &mock_ctx,
11717                version,
11718                sketch_id,
11719                constraint_id,
11720                label_position,
11721                vec![point0_id],
11722            )
11723            .await
11724            .unwrap();
11725
11726        assert_point_position_close(
11727            point_position(&scene_delta.new_graph, point0_id),
11728            point0_after_segment_edit,
11729        );
11730        assert_point_position_close(
11731            point_position(&scene_delta.new_graph, point1_id),
11732            point1_after_segment_edit,
11733        );
11734
11735        mock_ctx.close().await;
11736    }
11737
11738    #[tokio::test(flavor = "multi_thread")]
11739    async fn test_distance_point_line() {
11740        let initial_source = "\
11741sketch(on = XY) {
11742  point(at = [var 0, var 5])
11743  line(start = [var 0, var 0], end = [var 10, var 0])
11744}
11745";
11746
11747        let program = Program::parse(initial_source).unwrap().0.unwrap();
11748
11749        let mut frontend = FrontendState::new();
11750
11751        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11752        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11753        let version = Version(0);
11754
11755        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11756        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11757        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11758        let sketch_id = sketch_object.id;
11759        let sketch = expect_sketch(sketch_object);
11760        let point_id = *sketch.segments.first().unwrap();
11761        let line_id = *sketch
11762            .segments
11763            .iter()
11764            .find(|segment_id| {
11765                matches!(
11766                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11767                    Some(ObjectKind::Segment {
11768                        segment: Segment::Line(_)
11769                    })
11770                )
11771            })
11772            .unwrap();
11773
11774        let label_position = Point2d {
11775            x: Number {
11776                value: 10.0,
11777                units: NumericSuffix::Mm,
11778            },
11779            y: Number {
11780                value: 11.0,
11781                units: NumericSuffix::Mm,
11782            },
11783        };
11784        let constraint = Constraint::Distance(Distance {
11785            segments: vec![point_id.into(), line_id.into()],
11786            distance: Number {
11787                value: 5.0,
11788                units: NumericSuffix::Mm,
11789            },
11790            label_position: Some(label_position.clone()),
11791            source: Default::default(),
11792        });
11793        let (src_delta, scene_delta) = frontend
11794            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11795            .await
11796            .unwrap();
11797        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11798        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11799        let sketch = expect_sketch(sketch_object);
11800        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11801        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11802            panic!("Expected constraint object");
11803        };
11804        let Constraint::Distance(distance) = constraint else {
11805            panic!("Expected distance constraint");
11806        };
11807        assert_eq!(distance.label_position, Some(label_position));
11808
11809        ctx.close().await;
11810        mock_ctx.close().await;
11811    }
11812
11813    #[tokio::test(flavor = "multi_thread")]
11814    async fn test_distance_point_arc() {
11815        let initial_source = "\
11816sketch(on = XY) {
11817  point(at = [var 0, var 8])
11818  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11819}
11820";
11821
11822        let program = Program::parse(initial_source).unwrap().0.unwrap();
11823
11824        let mut frontend = FrontendState::new();
11825
11826        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11827        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11828        let version = Version(0);
11829
11830        frontend.hack_set_program(&ctx, program).await.unwrap();
11831        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11832        let sketch_id = sketch_object.id;
11833        let sketch = expect_sketch(sketch_object);
11834        let point_id = *sketch.segments.first().unwrap();
11835        let arc_id = *sketch
11836            .segments
11837            .iter()
11838            .find(|segment_id| {
11839                matches!(
11840                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11841                    Some(ObjectKind::Segment {
11842                        segment: Segment::Arc(_)
11843                    })
11844                )
11845            })
11846            .unwrap();
11847
11848        let constraint = Constraint::Distance(Distance {
11849            segments: vec![point_id.into(), arc_id.into()],
11850            distance: Number {
11851                value: 3.0,
11852                units: NumericSuffix::Mm,
11853            },
11854            label_position: None,
11855            source: Default::default(),
11856        });
11857        let (src_delta, _scene_delta) = frontend
11858            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11859            .await
11860            .unwrap();
11861        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11862
11863        ctx.close().await;
11864        mock_ctx.close().await;
11865    }
11866
11867    #[tokio::test(flavor = "multi_thread")]
11868    async fn test_distance_arc_origin() {
11869        let initial_source = "\
11870sketch001 = sketch(on = XY) {
11871  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11872}
11873";
11874
11875        let program = Program::parse(initial_source).unwrap().0.unwrap();
11876
11877        let mut frontend = FrontendState::new();
11878
11879        let mock_ctx = ExecutorContext::new_mock(None).await;
11880        let version = Version(0);
11881
11882        frontend.program = program.clone();
11883        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11884        frontend.update_state_after_exec(outcome, true);
11885        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11886        let sketch_id = sketch_object.id;
11887        let sketch = expect_sketch(sketch_object);
11888        let arc_id = *sketch
11889            .segments
11890            .iter()
11891            .find(|segment_id| {
11892                matches!(
11893                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11894                    Some(ObjectKind::Segment {
11895                        segment: Segment::Arc(_)
11896                    })
11897                )
11898            })
11899            .unwrap();
11900
11901        let constraint = Constraint::Distance(Distance {
11902            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11903            distance: Number {
11904                value: 3.0,
11905                units: NumericSuffix::Mm,
11906            },
11907            label_position: None,
11908            source: Default::default(),
11909        });
11910        let (src_delta, _scene_delta) = frontend
11911            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11912            .await
11913            .unwrap();
11914        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11915
11916        mock_ctx.close().await;
11917    }
11918
11919    #[tokio::test(flavor = "multi_thread")]
11920    async fn test_distance_line_origin() {
11921        let initial_source = "\
11922sketch(on = XY) {
11923  line(start = [var 5, var 0], end = [var 5, var 10])
11924}
11925";
11926
11927        let program = Program::parse(initial_source).unwrap().0.unwrap();
11928
11929        let mut frontend = FrontendState::new();
11930
11931        let mock_ctx = ExecutorContext::new_mock(None).await;
11932        let version = Version(0);
11933
11934        frontend.program = program.clone();
11935        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11936        frontend.update_state_after_exec(outcome, true);
11937        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11938        let sketch_id = sketch_object.id;
11939        let sketch = expect_sketch(sketch_object);
11940        let line_id = *sketch
11941            .segments
11942            .iter()
11943            .find(|segment_id| {
11944                matches!(
11945                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11946                    Some(ObjectKind::Segment {
11947                        segment: Segment::Line(_)
11948                    })
11949                )
11950            })
11951            .unwrap();
11952
11953        let constraint = Constraint::Distance(Distance {
11954            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11955            distance: Number {
11956                value: 5.0,
11957                units: NumericSuffix::Mm,
11958            },
11959            label_position: None,
11960            source: Default::default(),
11961        });
11962        let (src_delta, _scene_delta) = frontend
11963            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11964            .await
11965            .unwrap();
11966        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11967
11968        mock_ctx.close().await;
11969    }
11970
11971    #[tokio::test(flavor = "multi_thread")]
11972    async fn test_distance_line_circle() {
11973        let initial_source = "\
11974sketch(on = XY) {
11975  line(start = [var -10, var 8], end = [var 10, var 8])
11976  circle(start = [var 5, var 0], center = [var 0, var 0])
11977}
11978";
11979
11980        let program = Program::parse(initial_source).unwrap().0.unwrap();
11981
11982        let mut frontend = FrontendState::new();
11983
11984        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11985        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11986        let version = Version(0);
11987
11988        frontend.hack_set_program(&ctx, program).await.unwrap();
11989        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11990        let sketch_id = sketch_object.id;
11991        let sketch = expect_sketch(sketch_object);
11992        let line_id = *sketch
11993            .segments
11994            .iter()
11995            .find(|segment_id| {
11996                matches!(
11997                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11998                    Some(ObjectKind::Segment {
11999                        segment: Segment::Line(_)
12000                    })
12001                )
12002            })
12003            .unwrap();
12004        let circle_id = *sketch
12005            .segments
12006            .iter()
12007            .find(|segment_id| {
12008                matches!(
12009                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12010                    Some(ObjectKind::Segment {
12011                        segment: Segment::Circle(_)
12012                    })
12013                )
12014            })
12015            .unwrap();
12016
12017        let constraint = Constraint::Distance(Distance {
12018            segments: vec![line_id.into(), circle_id.into()],
12019            distance: Number {
12020                value: 3.0,
12021                units: NumericSuffix::Mm,
12022            },
12023            label_position: None,
12024            source: Default::default(),
12025        });
12026        let (src_delta, _scene_delta) = frontend
12027            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12028            .await
12029            .unwrap();
12030        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12031
12032        ctx.close().await;
12033        mock_ctx.close().await;
12034    }
12035
12036    #[tokio::test(flavor = "multi_thread")]
12037    async fn test_distance_circle_arc() {
12038        let initial_source = "\
12039sketch(on = XY) {
12040  circle(start = [var 5, var 0], center = [var 0, var 0])
12041  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12042}
12043";
12044
12045        let program = Program::parse(initial_source).unwrap().0.unwrap();
12046
12047        let mut frontend = FrontendState::new();
12048
12049        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12050        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12051        let version = Version(0);
12052
12053        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12054        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12055        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12056        let sketch_id = sketch_object.id;
12057        let sketch = expect_sketch(sketch_object);
12058        let circle_id = *sketch
12059            .segments
12060            .iter()
12061            .find(|segment_id| {
12062                matches!(
12063                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12064                    Some(ObjectKind::Segment {
12065                        segment: Segment::Circle(_)
12066                    })
12067                )
12068            })
12069            .unwrap();
12070        let arc_id = *sketch
12071            .segments
12072            .iter()
12073            .find(|segment_id| {
12074                matches!(
12075                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12076                    Some(ObjectKind::Segment {
12077                        segment: Segment::Arc(_)
12078                    })
12079                )
12080            })
12081            .unwrap();
12082
12083        let constraint = Constraint::Distance(Distance {
12084            segments: vec![circle_id.into(), arc_id.into()],
12085            distance: Number {
12086                value: 3.0,
12087                units: NumericSuffix::Mm,
12088            },
12089            label_position: None,
12090            source: Default::default(),
12091        });
12092        let (src_delta, _scene_delta) = frontend
12093            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12094            .await
12095            .unwrap();
12096        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12097
12098        ctx.close().await;
12099        mock_ctx.close().await;
12100    }
12101
12102    #[tokio::test(flavor = "multi_thread")]
12103    async fn test_distance_parallel_lines() {
12104        let initial_source = "\
12105sketch(on = XY) {
12106  line(start = [var 0, var 0], end = [var 10, var 0])
12107  line(start = [var 0, var 5], end = [var 10, var 5])
12108}
12109";
12110
12111        let program = Program::parse(initial_source).unwrap().0.unwrap();
12112
12113        let mut frontend = FrontendState::new();
12114
12115        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12116        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12117        let version = Version(0);
12118
12119        frontend.hack_set_program(&ctx, program).await.unwrap();
12120        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12121        let sketch_id = sketch_object.id;
12122        let sketch = expect_sketch(sketch_object);
12123        let line_ids = sketch
12124            .segments
12125            .iter()
12126            .copied()
12127            .filter(|segment_id| {
12128                matches!(
12129                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12130                    Some(ObjectKind::Segment {
12131                        segment: Segment::Line(_)
12132                    })
12133                )
12134            })
12135            .collect::<Vec<_>>();
12136
12137        let constraint = Constraint::Distance(Distance {
12138            segments: vec![line_ids[0].into(), line_ids[1].into()],
12139            distance: Number {
12140                value: 5.0,
12141                units: NumericSuffix::Mm,
12142            },
12143            label_position: None,
12144            source: Default::default(),
12145        });
12146        let (src_delta, _scene_delta) = frontend
12147            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12148            .await
12149            .unwrap();
12150        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12151
12152        ctx.close().await;
12153        mock_ctx.close().await;
12154    }
12155
12156    #[tokio::test(flavor = "multi_thread")]
12157    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12158        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12159        // one another because the gradient of the residual has no tangential component in this
12160        // configuration. The only path to reducing the residual is shrinking the lengths of the
12161        // lines which are causing the lines to collapse, producing a degenerate output.
12162        let initial_source = "\
12163sketch(on = XY) {
12164  line(start = [var 0, var 0], end = [var 10, var 0])
12165  line(start = [var 0, var 0], end = [var 10, var 10])
12166}
12167";
12168
12169        let program = Program::parse(initial_source).unwrap().0.unwrap();
12170
12171        let mut frontend = FrontendState::new();
12172
12173        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12174        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12175        let version = Version(0);
12176
12177        frontend.hack_set_program(&ctx, program).await.unwrap();
12178        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12179        let sketch_id = sketch_object.id;
12180        let sketch = expect_sketch(sketch_object);
12181        let line_ids = sketch
12182            .segments
12183            .iter()
12184            .copied()
12185            .filter(|segment_id| {
12186                matches!(
12187                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12188                    Some(ObjectKind::Segment {
12189                        segment: Segment::Line(_)
12190                    })
12191                )
12192            })
12193            .collect::<Vec<_>>();
12194
12195        let constraint = Constraint::Distance(Distance {
12196            segments: vec![line_ids[0].into(), line_ids[1].into()],
12197            distance: Number {
12198                value: 5.0,
12199                units: NumericSuffix::Mm,
12200            },
12201            label_position: None,
12202            source: Default::default(),
12203        });
12204        let (src_delta, _scene_delta) = frontend
12205            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12206            .await
12207            .unwrap();
12208        insta::assert_snapshot!(
12209            "test_distance_non_parallel_lines_lowers_to_distance",
12210            src_delta.text.as_str()
12211        );
12212
12213        ctx.close().await;
12214        mock_ctx.close().await;
12215    }
12216
12217    #[tokio::test(flavor = "multi_thread")]
12218    async fn test_horizontal_distance_two_points() {
12219        let initial_source = "\
12220sketch(on = XY) {
12221  point(at = [var 1, var 2])
12222  point(at = [var 3, var 4])
12223}
12224";
12225
12226        let program = Program::parse(initial_source).unwrap().0.unwrap();
12227
12228        let mut frontend = FrontendState::new();
12229
12230        let mock_ctx = ExecutorContext::new_mock(None).await;
12231        let version = Version(0);
12232
12233        frontend.program = program.clone();
12234        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12235        frontend.update_state_after_exec(outcome, true);
12236        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12237        let sketch_id = sketch_object.id;
12238        let sketch = expect_sketch(sketch_object);
12239        let point0_id = *sketch.segments.first().unwrap();
12240        let point1_id = *sketch.segments.get(1).unwrap();
12241        let label_position = Point2d {
12242            x: Number {
12243                value: 10.0,
12244                units: NumericSuffix::Mm,
12245            },
12246            y: Number {
12247                value: 11.0,
12248                units: NumericSuffix::Mm,
12249            },
12250        };
12251
12252        let constraint = Constraint::HorizontalDistance(Distance {
12253            segments: vec![point0_id.into(), point1_id.into()],
12254            distance: Number {
12255                value: 2.0,
12256                units: NumericSuffix::Mm,
12257            },
12258            label_position: Some(label_position.clone()),
12259            source: Default::default(),
12260        });
12261        let (src_delta, scene_delta) = frontend
12262            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12263            .await
12264            .unwrap();
12265        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12266        assert_eq!(
12267            scene_delta.new_graph.objects.len(),
12268            5,
12269            "{:#?}",
12270            scene_delta.new_graph.objects
12271        );
12272        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12273        let sketch = expect_sketch(sketch_object);
12274        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12275        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12276            panic!("Expected constraint object");
12277        };
12278        let Constraint::HorizontalDistance(distance) = constraint else {
12279            panic!("Expected horizontal distance constraint");
12280        };
12281        assert_eq!(distance.label_position, Some(label_position));
12282
12283        mock_ctx.close().await;
12284    }
12285
12286    #[tokio::test(flavor = "multi_thread")]
12287    async fn test_radius_single_arc_segment() {
12288        let initial_source = "\
12289sketch(on = XY) {
12290  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12291}
12292";
12293
12294        let program = Program::parse(initial_source).unwrap().0.unwrap();
12295
12296        let mut frontend = FrontendState::new();
12297
12298        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12299        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12300        let version = Version(0);
12301
12302        frontend.hack_set_program(&ctx, program).await.unwrap();
12303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12304        let sketch_id = sketch_object.id;
12305        let sketch = expect_sketch(sketch_object);
12306        // Find the arc segment (not the points)
12307        let arc_id = sketch
12308            .segments
12309            .iter()
12310            .find(|&seg_id| {
12311                let obj = frontend.scene_graph.objects.get(seg_id.0);
12312                matches!(
12313                    obj.map(|o| &o.kind),
12314                    Some(ObjectKind::Segment {
12315                        segment: Segment::Arc(_)
12316                    })
12317                )
12318            })
12319            .unwrap();
12320
12321        let constraint = Constraint::Radius(Radius {
12322            arc: *arc_id,
12323            radius: Number {
12324                value: 5.0,
12325                units: NumericSuffix::Mm,
12326            },
12327            label_position: None,
12328            source: Default::default(),
12329        });
12330        let (src_delta, scene_delta) = frontend
12331            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12332            .await
12333            .unwrap();
12334        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12335        assert_eq!(
12336            scene_delta.new_graph.objects.len(),
12337            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12338            "{:#?}",
12339            scene_delta.new_graph.objects
12340        );
12341
12342        ctx.close().await;
12343        mock_ctx.close().await;
12344    }
12345
12346    #[tokio::test(flavor = "multi_thread")]
12347    async fn test_radius_single_arc_segment_with_label_position() {
12348        let initial_source = "\
12349sketch(on = XY) {
12350  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12351}
12352";
12353
12354        let program = Program::parse(initial_source).unwrap().0.unwrap();
12355        let mut frontend = FrontendState::new();
12356        let mock_ctx = ExecutorContext::new_mock(None).await;
12357        let version = Version(0);
12358
12359        frontend.program = program.clone();
12360        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12361        frontend.update_state_after_exec(outcome, true);
12362        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12363        let sketch_id = sketch_object.id;
12364        let sketch = expect_sketch(sketch_object);
12365        let arc_id = sketch
12366            .segments
12367            .iter()
12368            .find(|&seg_id| {
12369                let obj = frontend.scene_graph.objects.get(seg_id.0);
12370                matches!(
12371                    obj.map(|o| &o.kind),
12372                    Some(ObjectKind::Segment {
12373                        segment: Segment::Arc(_)
12374                    })
12375                )
12376            })
12377            .unwrap();
12378
12379        let label_position = Point2d {
12380            x: Number {
12381                value: 10.0,
12382                units: NumericSuffix::Mm,
12383            },
12384            y: Number {
12385                value: 11.0,
12386                units: NumericSuffix::Mm,
12387            },
12388        };
12389        let constraint = Constraint::Radius(Radius {
12390            arc: *arc_id,
12391            radius: Number {
12392                value: 5.0,
12393                units: NumericSuffix::Mm,
12394            },
12395            label_position: Some(label_position.clone()),
12396            source: Default::default(),
12397        });
12398        let (src_delta, scene_delta) = frontend
12399            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12400            .await
12401            .unwrap();
12402        insta::assert_snapshot!(
12403            "test_radius_single_arc_segment_with_label_position",
12404            src_delta.text.as_str()
12405        );
12406
12407        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12408        let sketch = expect_sketch(sketch_object);
12409        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12410        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12411            panic!("Expected constraint object");
12412        };
12413        let Constraint::Radius(radius) = constraint else {
12414            panic!("Expected radius constraint");
12415        };
12416        assert_eq!(radius.label_position, Some(label_position));
12417
12418        mock_ctx.close().await;
12419    }
12420
12421    #[tokio::test(flavor = "multi_thread")]
12422    async fn test_edit_radius_constraint_label_position() {
12423        let initial_source = "\
12424sketch(on = XY) {
12425  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12426  radius(arc1) == 5mm
12427}
12428";
12429
12430        let program = Program::parse(initial_source).unwrap().0.unwrap();
12431        let mut frontend = FrontendState::new();
12432        let mock_ctx = ExecutorContext::new_mock(None).await;
12433        let version = Version(0);
12434
12435        frontend.program = program.clone();
12436        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12437        frontend.update_state_after_exec(outcome, true);
12438        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12439        let sketch_id = sketch_object.id;
12440        let sketch = expect_sketch(sketch_object);
12441        let constraint_id = sketch.constraints[0];
12442        let label_position = Point2d {
12443            x: Number {
12444                value: 10.0,
12445                units: NumericSuffix::Mm,
12446            },
12447            y: Number {
12448                value: 11.0,
12449                units: NumericSuffix::Mm,
12450            },
12451        };
12452
12453        let (src_delta, scene_delta) = frontend
12454            .edit_distance_constraint_label_position(
12455                &mock_ctx,
12456                version,
12457                sketch_id,
12458                constraint_id,
12459                label_position.clone(),
12460                vec![],
12461            )
12462            .await
12463            .unwrap();
12464        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12465
12466        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12467        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12468            panic!("Expected constraint object");
12469        };
12470        let Constraint::Radius(radius) = constraint else {
12471            panic!("Expected radius constraint");
12472        };
12473        assert_eq!(radius.label_position, Some(label_position));
12474
12475        mock_ctx.close().await;
12476    }
12477
12478    #[tokio::test(flavor = "multi_thread")]
12479    async fn test_vertical_distance_two_points() {
12480        let initial_source = "\
12481sketch(on = XY) {
12482  point(at = [var 1, var 2])
12483  point(at = [var 3, var 4])
12484}
12485";
12486
12487        let program = Program::parse(initial_source).unwrap().0.unwrap();
12488
12489        let mut frontend = FrontendState::new();
12490
12491        let mock_ctx = ExecutorContext::new_mock(None).await;
12492        let version = Version(0);
12493
12494        frontend.program = program.clone();
12495        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12496        frontend.update_state_after_exec(outcome, true);
12497        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12498        let sketch_id = sketch_object.id;
12499        let sketch = expect_sketch(sketch_object);
12500        let point0_id = *sketch.segments.first().unwrap();
12501        let point1_id = *sketch.segments.get(1).unwrap();
12502        let label_position = Point2d {
12503            x: Number {
12504                value: 10.0,
12505                units: NumericSuffix::Mm,
12506            },
12507            y: Number {
12508                value: 11.0,
12509                units: NumericSuffix::Mm,
12510            },
12511        };
12512
12513        let constraint = Constraint::VerticalDistance(Distance {
12514            segments: vec![point0_id.into(), point1_id.into()],
12515            distance: Number {
12516                value: 2.0,
12517                units: NumericSuffix::Mm,
12518            },
12519            label_position: Some(label_position.clone()),
12520            source: Default::default(),
12521        });
12522        let (src_delta, scene_delta) = frontend
12523            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12524            .await
12525            .unwrap();
12526        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12527        assert_eq!(
12528            scene_delta.new_graph.objects.len(),
12529            5,
12530            "{:#?}",
12531            scene_delta.new_graph.objects
12532        );
12533        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12534        let sketch = expect_sketch(sketch_object);
12535        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12536        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12537            panic!("Expected constraint object");
12538        };
12539        let Constraint::VerticalDistance(distance) = constraint else {
12540            panic!("Expected vertical distance constraint");
12541        };
12542        assert_eq!(distance.label_position, Some(label_position));
12543
12544        mock_ctx.close().await;
12545    }
12546
12547    #[tokio::test(flavor = "multi_thread")]
12548    async fn test_add_fixed_standalone_point() {
12549        let initial_source = "\
12550sketch(on = XY) {
12551  point(at = [var 1, var 2])
12552}
12553";
12554
12555        let program = Program::parse(initial_source).unwrap().0.unwrap();
12556
12557        let mut frontend = FrontendState::new();
12558
12559        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12560        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12561        let version = Version(0);
12562
12563        frontend.hack_set_program(&ctx, program).await.unwrap();
12564        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12565        let sketch_id = sketch_object.id;
12566        let sketch = expect_sketch(sketch_object);
12567        let point_id = *sketch.segments.first().unwrap();
12568
12569        let (src_delta, scene_delta) = frontend
12570            .add_constraint(
12571                &mock_ctx,
12572                version,
12573                sketch_id,
12574                Constraint::Fixed(Fixed {
12575                    points: vec![FixedPoint {
12576                        point: point_id,
12577                        position: Point2d {
12578                            x: Number {
12579                                value: 2.0,
12580                                units: NumericSuffix::Mm,
12581                            },
12582                            y: Number {
12583                                value: 3.0,
12584                                units: NumericSuffix::Mm,
12585                            },
12586                        },
12587                    }],
12588                }),
12589            )
12590            .await
12591            .unwrap();
12592        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12593        assert_eq!(
12594            scene_delta.new_graph.objects.len(),
12595            4,
12596            "{:#?}",
12597            scene_delta.new_graph.objects
12598        );
12599
12600        ctx.close().await;
12601        mock_ctx.close().await;
12602    }
12603
12604    #[tokio::test(flavor = "multi_thread")]
12605    async fn test_add_fixed_multiple_points() {
12606        let initial_source = "\
12607sketch(on = XY) {
12608  point(at = [var 1, var 2])
12609  point(at = [var 3, var 4])
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_geometry_only_with_default_client().await.unwrap();
12618        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12619        let version = Version(0);
12620
12621        frontend.hack_set_program(&ctx, program).await.unwrap();
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 point0_id = *sketch.segments.first().unwrap();
12626        let point1_id = *sketch.segments.get(1).unwrap();
12627
12628        let (src_delta, scene_delta) = frontend
12629            .add_constraint(
12630                &mock_ctx,
12631                version,
12632                sketch_id,
12633                Constraint::Fixed(Fixed {
12634                    points: vec![
12635                        FixedPoint {
12636                            point: point0_id,
12637                            position: Point2d {
12638                                x: Number {
12639                                    value: 2.0,
12640                                    units: NumericSuffix::Mm,
12641                                },
12642                                y: Number {
12643                                    value: 3.0,
12644                                    units: NumericSuffix::Mm,
12645                                },
12646                            },
12647                        },
12648                        FixedPoint {
12649                            point: point1_id,
12650                            position: Point2d {
12651                                x: Number {
12652                                    value: 4.0,
12653                                    units: NumericSuffix::Mm,
12654                                },
12655                                y: Number {
12656                                    value: 5.0,
12657                                    units: NumericSuffix::Mm,
12658                                },
12659                            },
12660                        },
12661                    ],
12662                }),
12663            )
12664            .await
12665            .unwrap();
12666        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12667        assert_eq!(
12668            scene_delta.new_graph.objects.len(),
12669            6,
12670            "{:#?}",
12671            scene_delta.new_graph.objects
12672        );
12673
12674        ctx.close().await;
12675        mock_ctx.close().await;
12676    }
12677
12678    #[tokio::test(flavor = "multi_thread")]
12679    async fn test_add_fixed_owned_point() {
12680        let initial_source = "\
12681sketch(on = XY) {
12682  line(start = [var 1, var 2], end = [var 3, var 4])
12683}
12684";
12685
12686        let program = Program::parse(initial_source).unwrap().0.unwrap();
12687
12688        let mut frontend = FrontendState::new();
12689
12690        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12691        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12692        let version = Version(0);
12693
12694        frontend.hack_set_program(&ctx, program).await.unwrap();
12695        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12696        let sketch_id = sketch_object.id;
12697        let sketch = expect_sketch(sketch_object);
12698        let line_start_id = *sketch.segments.first().unwrap();
12699
12700        let (src_delta, scene_delta) = frontend
12701            .add_constraint(
12702                &mock_ctx,
12703                version,
12704                sketch_id,
12705                Constraint::Fixed(Fixed {
12706                    points: vec![FixedPoint {
12707                        point: line_start_id,
12708                        position: Point2d {
12709                            x: Number {
12710                                value: 2.0,
12711                                units: NumericSuffix::Mm,
12712                            },
12713                            y: Number {
12714                                value: 3.0,
12715                                units: NumericSuffix::Mm,
12716                            },
12717                        },
12718                    }],
12719                }),
12720            )
12721            .await
12722            .unwrap();
12723        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12724        assert_eq!(
12725            scene_delta.new_graph.objects.len(),
12726            6,
12727            "{:#?}",
12728            scene_delta.new_graph.objects
12729        );
12730
12731        ctx.close().await;
12732        mock_ctx.close().await;
12733    }
12734
12735    #[tokio::test(flavor = "multi_thread")]
12736    async fn test_radius_error_cases() {
12737        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12738        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12739        let version = Version(0);
12740
12741        // Test: Single point should error
12742        let initial_source_point = "\
12743sketch(on = XY) {
12744  point(at = [var 1, var 2])
12745}
12746";
12747        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12748        let mut frontend_point = FrontendState::new();
12749        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12750        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12751        let sketch_id_point = sketch_object_point.id;
12752        let sketch_point = expect_sketch(sketch_object_point);
12753        let point_id = *sketch_point.segments.first().unwrap();
12754
12755        let constraint_point = Constraint::Radius(Radius {
12756            arc: point_id,
12757            radius: Number {
12758                value: 5.0,
12759                units: NumericSuffix::Mm,
12760            },
12761            label_position: None,
12762            source: Default::default(),
12763        });
12764        let result_point = frontend_point
12765            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12766            .await;
12767        assert!(result_point.is_err(), "Single point should error for radius");
12768
12769        // Test: Single line segment should error (only arc segments supported)
12770        let initial_source_line = "\
12771sketch(on = XY) {
12772  line(start = [var 1, var 2], end = [var 3, var 4])
12773}
12774";
12775        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12776        let mut frontend_line = FrontendState::new();
12777        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12778        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12779        let sketch_id_line = sketch_object_line.id;
12780        let sketch_line = expect_sketch(sketch_object_line);
12781        let line_id = *sketch_line.segments.first().unwrap();
12782
12783        let constraint_line = Constraint::Radius(Radius {
12784            arc: line_id,
12785            radius: Number {
12786                value: 5.0,
12787                units: NumericSuffix::Mm,
12788            },
12789            label_position: None,
12790            source: Default::default(),
12791        });
12792        let result_line = frontend_line
12793            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12794            .await;
12795        assert!(result_line.is_err(), "Single line segment should error for radius");
12796
12797        ctx.close().await;
12798        mock_ctx.close().await;
12799    }
12800
12801    #[tokio::test(flavor = "multi_thread")]
12802    async fn test_diameter_single_arc_segment() {
12803        let initial_source = "\
12804sketch(on = XY) {
12805  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12806}
12807";
12808
12809        let program = Program::parse(initial_source).unwrap().0.unwrap();
12810
12811        let mut frontend = FrontendState::new();
12812
12813        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12814        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12815        let version = Version(0);
12816
12817        frontend.hack_set_program(&ctx, program).await.unwrap();
12818        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12819        let sketch_id = sketch_object.id;
12820        let sketch = expect_sketch(sketch_object);
12821        // Find the arc segment (not the points)
12822        let arc_id = sketch
12823            .segments
12824            .iter()
12825            .find(|&seg_id| {
12826                let obj = frontend.scene_graph.objects.get(seg_id.0);
12827                matches!(
12828                    obj.map(|o| &o.kind),
12829                    Some(ObjectKind::Segment {
12830                        segment: Segment::Arc(_)
12831                    })
12832                )
12833            })
12834            .unwrap();
12835
12836        let constraint = Constraint::Diameter(Diameter {
12837            arc: *arc_id,
12838            diameter: Number {
12839                value: 10.0,
12840                units: NumericSuffix::Mm,
12841            },
12842            label_position: None,
12843            source: Default::default(),
12844        });
12845        let (src_delta, scene_delta) = frontend
12846            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12847            .await
12848            .unwrap();
12849        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12850        assert_eq!(
12851            scene_delta.new_graph.objects.len(),
12852            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12853            "{:#?}",
12854            scene_delta.new_graph.objects
12855        );
12856
12857        ctx.close().await;
12858        mock_ctx.close().await;
12859    }
12860
12861    #[tokio::test(flavor = "multi_thread")]
12862    async fn test_diameter_single_arc_segment_with_label_position() {
12863        let initial_source = "\
12864sketch(on = XY) {
12865  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12866}
12867";
12868
12869        let program = Program::parse(initial_source).unwrap().0.unwrap();
12870        let mut frontend = FrontendState::new();
12871        let mock_ctx = ExecutorContext::new_mock(None).await;
12872        let version = Version(0);
12873
12874        frontend.program = program.clone();
12875        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12876        frontend.update_state_after_exec(outcome, true);
12877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878        let sketch_id = sketch_object.id;
12879        let sketch = expect_sketch(sketch_object);
12880        let arc_id = sketch
12881            .segments
12882            .iter()
12883            .find(|&seg_id| {
12884                let obj = frontend.scene_graph.objects.get(seg_id.0);
12885                matches!(
12886                    obj.map(|o| &o.kind),
12887                    Some(ObjectKind::Segment {
12888                        segment: Segment::Arc(_)
12889                    })
12890                )
12891            })
12892            .unwrap();
12893
12894        let label_position = Point2d {
12895            x: Number {
12896                value: 10.0,
12897                units: NumericSuffix::Mm,
12898            },
12899            y: Number {
12900                value: 11.0,
12901                units: NumericSuffix::Mm,
12902            },
12903        };
12904        let constraint = Constraint::Diameter(Diameter {
12905            arc: *arc_id,
12906            diameter: Number {
12907                value: 10.0,
12908                units: NumericSuffix::Mm,
12909            },
12910            label_position: Some(label_position.clone()),
12911            source: Default::default(),
12912        });
12913        let (src_delta, scene_delta) = frontend
12914            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12915            .await
12916            .unwrap();
12917        insta::assert_snapshot!(
12918            "test_diameter_single_arc_segment_with_label_position",
12919            src_delta.text.as_str()
12920        );
12921
12922        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12923        let sketch = expect_sketch(sketch_object);
12924        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12925        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12926            panic!("Expected constraint object");
12927        };
12928        let Constraint::Diameter(diameter) = constraint else {
12929            panic!("Expected diameter constraint");
12930        };
12931        assert_eq!(diameter.label_position, Some(label_position));
12932
12933        mock_ctx.close().await;
12934    }
12935
12936    #[tokio::test(flavor = "multi_thread")]
12937    async fn test_edit_diameter_constraint_label_position() {
12938        let initial_source = "\
12939sketch(on = XY) {
12940  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12941  diameter(arc1) == 10mm
12942}
12943";
12944
12945        let program = Program::parse(initial_source).unwrap().0.unwrap();
12946        let mut frontend = FrontendState::new();
12947        let mock_ctx = ExecutorContext::new_mock(None).await;
12948        let version = Version(0);
12949
12950        frontend.program = program.clone();
12951        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12952        frontend.update_state_after_exec(outcome, true);
12953        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12954        let sketch_id = sketch_object.id;
12955        let sketch = expect_sketch(sketch_object);
12956        let constraint_id = sketch.constraints[0];
12957        let label_position = Point2d {
12958            x: Number {
12959                value: 10.0,
12960                units: NumericSuffix::Mm,
12961            },
12962            y: Number {
12963                value: 11.0,
12964                units: NumericSuffix::Mm,
12965            },
12966        };
12967
12968        let (src_delta, scene_delta) = frontend
12969            .edit_distance_constraint_label_position(
12970                &mock_ctx,
12971                version,
12972                sketch_id,
12973                constraint_id,
12974                label_position.clone(),
12975                vec![],
12976            )
12977            .await
12978            .unwrap();
12979        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12980
12981        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12982        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12983            panic!("Expected constraint object");
12984        };
12985        let Constraint::Diameter(diameter) = constraint else {
12986            panic!("Expected diameter constraint");
12987        };
12988        assert_eq!(diameter.label_position, Some(label_position));
12989
12990        mock_ctx.close().await;
12991    }
12992
12993    #[tokio::test(flavor = "multi_thread")]
12994    async fn test_diameter_error_cases() {
12995        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12996        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12997        let version = Version(0);
12998
12999        // Test: Single point should error
13000        let initial_source_point = "\
13001sketch(on = XY) {
13002  point(at = [var 1, var 2])
13003}
13004";
13005        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13006        let mut frontend_point = FrontendState::new();
13007        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13008        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13009        let sketch_id_point = sketch_object_point.id;
13010        let sketch_point = expect_sketch(sketch_object_point);
13011        let point_id = *sketch_point.segments.first().unwrap();
13012
13013        let constraint_point = Constraint::Diameter(Diameter {
13014            arc: point_id,
13015            diameter: Number {
13016                value: 10.0,
13017                units: NumericSuffix::Mm,
13018            },
13019            label_position: None,
13020            source: Default::default(),
13021        });
13022        let result_point = frontend_point
13023            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13024            .await;
13025        assert!(result_point.is_err(), "Single point should error for diameter");
13026
13027        // Test: Single line segment should error (only arc segments supported)
13028        let initial_source_line = "\
13029sketch(on = XY) {
13030  line(start = [var 1, var 2], end = [var 3, var 4])
13031}
13032";
13033        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13034        let mut frontend_line = FrontendState::new();
13035        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13036        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13037        let sketch_id_line = sketch_object_line.id;
13038        let sketch_line = expect_sketch(sketch_object_line);
13039        let line_id = *sketch_line.segments.first().unwrap();
13040
13041        let constraint_line = Constraint::Diameter(Diameter {
13042            arc: line_id,
13043            diameter: Number {
13044                value: 10.0,
13045                units: NumericSuffix::Mm,
13046            },
13047            label_position: None,
13048            source: Default::default(),
13049        });
13050        let result_line = frontend_line
13051            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13052            .await;
13053        assert!(result_line.is_err(), "Single line segment should error for diameter");
13054
13055        ctx.close().await;
13056        mock_ctx.close().await;
13057    }
13058
13059    #[tokio::test(flavor = "multi_thread")]
13060    async fn test_line_horizontal() {
13061        let initial_source = "\
13062sketch(on = XY) {
13063  line(start = [var 1, var 2], end = [var 3, var 4])
13064}
13065";
13066
13067        let program = Program::parse(initial_source).unwrap().0.unwrap();
13068
13069        let mut frontend = FrontendState::new();
13070
13071        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13072        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13073        let version = Version(0);
13074
13075        frontend.hack_set_program(&ctx, program).await.unwrap();
13076        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13077        let sketch_id = sketch_object.id;
13078        let sketch = expect_sketch(sketch_object);
13079        let line1_id = *sketch.segments.get(2).unwrap();
13080
13081        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13082        let (src_delta, scene_delta) = frontend
13083            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13084            .await
13085            .unwrap();
13086        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13087        assert_eq!(
13088            scene_delta.new_graph.objects.len(),
13089            6,
13090            "{:#?}",
13091            scene_delta.new_graph.objects
13092        );
13093
13094        ctx.close().await;
13095        mock_ctx.close().await;
13096    }
13097
13098    #[tokio::test(flavor = "multi_thread")]
13099    async fn test_control_point_spline_edge_horizontal() {
13100        let initial_source = "\
13101@settings(experimentalFeatures = allow)
13102splineSketch = sketch(on = XY) {
13103  controlPointSpline1 = controlPointSpline(points = [
13104    [var 0mm, var 0mm],
13105    [var 10mm, var 20mm],
13106    [var 20mm, var 0mm],
13107  ])
13108}
13109";
13110
13111        let program = Program::parse(initial_source).unwrap().0.unwrap();
13112
13113        let mut frontend = FrontendState::new();
13114
13115        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13116        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13117        let version = Version(0);
13118
13119        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13120        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13121        let sketch_id = sketch_object.id;
13122        let sketch = expect_sketch(sketch_object);
13123        let spline_id = sketch
13124            .segments
13125            .iter()
13126            .copied()
13127            .find(|seg_id| {
13128                matches!(
13129                    &frontend.scene_graph.objects[seg_id.0].kind,
13130                    ObjectKind::Segment {
13131                        segment: Segment::ControlPointSpline(_)
13132                    }
13133                )
13134            })
13135            .expect("Expected a control point spline segment in sketch");
13136        let edge_id = frontend
13137            .scene_graph
13138            .objects
13139            .iter()
13140            .find_map(|obj| match &obj.kind {
13141                ObjectKind::Segment {
13142                    segment: Segment::Line(line),
13143                } if line.owner == Some(spline_id) => Some(obj.id),
13144                _ => None,
13145            })
13146            .expect("Expected an owned control-polygon edge");
13147
13148        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13149        let (src_delta, _) = frontend
13150            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13151            .await
13152            .unwrap();
13153        assert!(
13154            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13155            "Expected horizontal constraint on spline edge, got: {}",
13156            src_delta.text
13157        );
13158
13159        ctx.close().await;
13160        mock_ctx.close().await;
13161    }
13162
13163    #[tokio::test(flavor = "multi_thread")]
13164    async fn test_control_point_spline_edge_angle() {
13165        let initial_source = "\
13166@settings(experimentalFeatures = allow)
13167splineSketch = sketch(on = XY) {
13168  controlPointSpline1 = controlPointSpline(points = [
13169    [var 0mm, var 0mm],
13170    [var 10mm, var 20mm],
13171    [var 20mm, var 0mm],
13172  ])
13173
13174  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13175}
13176";
13177
13178        let program = Program::parse(initial_source).unwrap().0.unwrap();
13179
13180        let mut frontend = FrontendState::new();
13181
13182        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13183        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13184        let version = Version(0);
13185
13186        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13187        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13188        let sketch_id = sketch_object.id;
13189        let sketch = expect_sketch(sketch_object);
13190        let spline_id = sketch
13191            .segments
13192            .iter()
13193            .copied()
13194            .find(|seg_id| {
13195                matches!(
13196                    &frontend.scene_graph.objects[seg_id.0].kind,
13197                    ObjectKind::Segment {
13198                        segment: Segment::ControlPointSpline(_)
13199                    }
13200                )
13201            })
13202            .expect("Expected a control point spline segment in sketch");
13203        let edge_id = frontend
13204            .scene_graph
13205            .objects
13206            .iter()
13207            .find_map(|obj| match &obj.kind {
13208                ObjectKind::Segment {
13209                    segment: Segment::Line(line),
13210                } if line.owner == Some(spline_id) => Some(obj.id),
13211                _ => None,
13212            })
13213            .expect("Expected an owned control-polygon edge");
13214        let line1_id = frontend
13215            .scene_graph
13216            .objects
13217            .iter()
13218            .find_map(|obj| match &obj.kind {
13219                ObjectKind::Segment {
13220                    segment: Segment::Line(line),
13221                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13222                _ => None,
13223            })
13224            .or_else(|| {
13225                sketch.segments.iter().copied().find(|seg_id| {
13226                    matches!(
13227                        &frontend.scene_graph.objects[seg_id.0].kind,
13228                        ObjectKind::Segment {
13229                            segment: Segment::Line(line),
13230                        } if line.owner.is_none()
13231                    )
13232                })
13233            })
13234            .expect("Expected a standalone line segment in sketch");
13235
13236        let constraint = Constraint::Angle(Angle {
13237            lines: vec![line1_id, edge_id],
13238            angle: Number {
13239                value: 30.0,
13240                units: NumericSuffix::Deg,
13241            },
13242            sector: None,
13243            inverse: None,
13244            label_position: None,
13245            source: Default::default(),
13246        });
13247        let (src_delta, _) = frontend
13248            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13249            .await
13250            .unwrap();
13251        assert!(
13252            src_delta
13253                .text
13254                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13255            "Expected angle constraint on spline edge, got: {}",
13256            src_delta.text
13257        );
13258
13259        ctx.close().await;
13260        mock_ctx.close().await;
13261    }
13262
13263    #[tokio::test(flavor = "multi_thread")]
13264    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13265        let initial_source = "\
13266@settings(experimentalFeatures = allow)
13267splineSketch = sketch(on = XY) {
13268  spline1 = controlPointSpline(points = [
13269    [var 0mm, var 0mm],
13270    [var 10mm, var 20mm],
13271    [var 20mm, var 0mm],
13272  ])
13273  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13274  coincident([spline1.controls[1], spline1.edges[0]])
13275  coincident([spline1.controls[0], line1])
13276}
13277";
13278
13279        let program = Program::parse(initial_source).unwrap().0.unwrap();
13280
13281        let mut frontend = FrontendState::new();
13282
13283        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13284        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13285
13286        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13287
13288        let ui_scene_graph = frontend.scene_graph_for_ui();
13289        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13290        let sketch = expect_sketch(sketch_object);
13291
13292        assert_eq!(
13293            sketch.constraints.len(),
13294            1,
13295            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13296        );
13297
13298        let visible_constraints = ui_scene_graph
13299            .objects
13300            .iter()
13301            .filter_map(|object| match &object.kind {
13302                ObjectKind::Constraint {
13303                    constraint: Constraint::Coincident(coincident),
13304                } => Some(coincident.clone()),
13305                _ => None,
13306            })
13307            .collect::<Vec<_>>();
13308
13309        assert_eq!(
13310            visible_constraints.len(),
13311            1,
13312            "Expected only one coincident constraint object in the UI scene graph"
13313        );
13314        assert_eq!(
13315            visible_constraints[0].get_segments().len(),
13316            2,
13317            "Expected the remaining visible coincident constraint to reference two segments"
13318        );
13319
13320        ctx.close().await;
13321        mock_ctx.close().await;
13322    }
13323
13324    #[tokio::test(flavor = "multi_thread")]
13325    async fn test_edit_control_point_spline_can_append_control_point() {
13326        let initial_source = "\
13327@settings(experimentalFeatures = allow)
13328splineSketch = sketch(on = XY) {
13329  controlPointSpline(points = [
13330    [var 0mm, var 0mm],
13331    [var 10mm, var 20mm],
13332    [var 20mm, var 0mm],
13333  ])
13334}
13335";
13336
13337        let program = Program::parse(initial_source).unwrap().0.unwrap();
13338
13339        let mut frontend = FrontendState::new();
13340
13341        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13342        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13343        let version = Version(0);
13344
13345        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13346        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13347        let sketch_id = sketch_object.id;
13348        let sketch = expect_sketch(sketch_object);
13349        let spline_id = sketch
13350            .segments
13351            .iter()
13352            .copied()
13353            .find(|seg_id| {
13354                matches!(
13355                    &frontend.scene_graph.objects[seg_id.0].kind,
13356                    ObjectKind::Segment {
13357                        segment: Segment::ControlPointSpline(_)
13358                    }
13359                )
13360            })
13361            .expect("Expected a control point spline segment in sketch");
13362
13363        let ctor = ControlPointSplineCtor {
13364            points: vec![
13365                Point2d {
13366                    x: Expr::Var(Number {
13367                        value: 0.0,
13368                        units: NumericSuffix::Mm,
13369                    }),
13370                    y: Expr::Var(Number {
13371                        value: 0.0,
13372                        units: NumericSuffix::Mm,
13373                    }),
13374                },
13375                Point2d {
13376                    x: Expr::Var(Number {
13377                        value: 10.0,
13378                        units: NumericSuffix::Mm,
13379                    }),
13380                    y: Expr::Var(Number {
13381                        value: 20.0,
13382                        units: NumericSuffix::Mm,
13383                    }),
13384                },
13385                Point2d {
13386                    x: Expr::Var(Number {
13387                        value: 20.0,
13388                        units: NumericSuffix::Mm,
13389                    }),
13390                    y: Expr::Var(Number {
13391                        value: 0.0,
13392                        units: NumericSuffix::Mm,
13393                    }),
13394                },
13395                Point2d {
13396                    x: Expr::Var(Number {
13397                        value: 30.0,
13398                        units: NumericSuffix::Mm,
13399                    }),
13400                    y: Expr::Var(Number {
13401                        value: 10.0,
13402                        units: NumericSuffix::Mm,
13403                    }),
13404                },
13405            ],
13406            construction: None,
13407        };
13408
13409        let segments = vec![ExistingSegmentCtor {
13410            id: spline_id,
13411            ctor: SegmentCtor::ControlPointSpline(ctor),
13412        }];
13413        let (src_delta, scene_delta) = frontend
13414            .edit_segments(&mock_ctx, version, sketch_id, segments)
13415            .await
13416            .unwrap();
13417
13418        assert!(
13419            src_delta.text.contains("[var 30mm, var 10mm]"),
13420            "Expected appended spline control point in source, got: {}",
13421            src_delta.text
13422        );
13423
13424        assert!(
13425            scene_delta.invalidates_ids,
13426            "Expected appending a spline control point to invalidate ids"
13427        );
13428        let updated_spline = scene_delta
13429            .new_graph
13430            .objects
13431            .iter()
13432            .find_map(|obj| match &obj.kind {
13433                ObjectKind::Segment {
13434                    segment: Segment::ControlPointSpline(updated_spline),
13435                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13436                _ => None,
13437            })
13438            .expect("Expected edited scene graph to contain a four-point control point spline");
13439        assert_eq!(
13440            updated_spline.controls.len(),
13441            4,
13442            "Expected edited spline to expose four control points"
13443        );
13444
13445        ctx.close().await;
13446        mock_ctx.close().await;
13447    }
13448
13449    #[tokio::test(flavor = "multi_thread")]
13450    async fn test_line_vertical() {
13451        let initial_source = "\
13452sketch(on = XY) {
13453  line(start = [var 1, var 2], end = [var 3, var 4])
13454}
13455";
13456
13457        let program = Program::parse(initial_source).unwrap().0.unwrap();
13458
13459        let mut frontend = FrontendState::new();
13460
13461        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13462        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13463        let version = Version(0);
13464
13465        frontend.hack_set_program(&ctx, program).await.unwrap();
13466        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13467        let sketch_id = sketch_object.id;
13468        let sketch = expect_sketch(sketch_object);
13469        let line1_id = *sketch.segments.get(2).unwrap();
13470
13471        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13472        let (src_delta, scene_delta) = frontend
13473            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13474            .await
13475            .unwrap();
13476        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13477        assert_eq!(
13478            scene_delta.new_graph.objects.len(),
13479            6,
13480            "{:#?}",
13481            scene_delta.new_graph.objects
13482        );
13483
13484        ctx.close().await;
13485        mock_ctx.close().await;
13486    }
13487
13488    #[tokio::test(flavor = "multi_thread")]
13489    async fn test_points_vertical() {
13490        let initial_source = "\
13491sketch001 = sketch(on = XY) {
13492  p0 = point(at = [var -2.23mm, var 3.1mm])
13493  pf = point(at = [4, 4])
13494}
13495";
13496
13497        let program = Program::parse(initial_source).unwrap().0.unwrap();
13498
13499        let mut frontend = FrontendState::new();
13500
13501        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13502        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13503        let version = Version(0);
13504
13505        frontend.hack_set_program(&ctx, program).await.unwrap();
13506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13507        let sketch_id = sketch_object.id;
13508        let sketch = expect_sketch(sketch_object);
13509        let point_ids = vec![
13510            sketch.segments.first().unwrap().to_owned(),
13511            sketch.segments.get(1).unwrap().to_owned(),
13512        ];
13513
13514        let constraint = Constraint::Vertical(Vertical::Points {
13515            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13516        });
13517        let (src_delta, scene_delta) = frontend
13518            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13519            .await
13520            .unwrap();
13521        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13522        assert_eq!(
13523            scene_delta.new_graph.objects.len(),
13524            5,
13525            "{:#?}",
13526            scene_delta.new_graph.objects
13527        );
13528
13529        ctx.close().await;
13530        mock_ctx.close().await;
13531    }
13532
13533    #[tokio::test(flavor = "multi_thread")]
13534    async fn test_points_horizontal() {
13535        let initial_source = "\
13536sketch001 = sketch(on = XY) {
13537  p0 = point(at = [var -2.23mm, var 3.1mm])
13538  pf = point(at = [4, 4])
13539}
13540";
13541
13542        let program = Program::parse(initial_source).unwrap().0.unwrap();
13543
13544        let mut frontend = FrontendState::new();
13545
13546        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13547        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13548        let version = Version(0);
13549
13550        frontend.hack_set_program(&ctx, program).await.unwrap();
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 point_ids = vec![
13555            sketch.segments.first().unwrap().to_owned(),
13556            sketch.segments.get(1).unwrap().to_owned(),
13557        ];
13558
13559        let constraint = Constraint::Horizontal(Horizontal::Points {
13560            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13561        });
13562        let (src_delta, scene_delta) = frontend
13563            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13564            .await
13565            .unwrap();
13566        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13567        assert_eq!(
13568            scene_delta.new_graph.objects.len(),
13569            5,
13570            "{:#?}",
13571            scene_delta.new_graph.objects
13572        );
13573
13574        ctx.close().await;
13575        mock_ctx.close().await;
13576    }
13577
13578    #[tokio::test(flavor = "multi_thread")]
13579    async fn test_point_horizontal_with_origin() {
13580        let initial_source = "\
13581sketch001 = sketch(on = XY) {
13582  p0 = point(at = [var -2.23mm, var 3.1mm])
13583}
13584";
13585
13586        let program = Program::parse(initial_source).unwrap().0.unwrap();
13587
13588        let mut frontend = FrontendState::new();
13589
13590        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13591        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13592        let version = Version(0);
13593
13594        frontend.hack_set_program(&ctx, program).await.unwrap();
13595        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13596        let sketch_id = sketch_object.id;
13597        let sketch = expect_sketch(sketch_object);
13598        let point_id = *sketch.segments.first().unwrap();
13599
13600        let constraint = Constraint::Horizontal(Horizontal::Points {
13601            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13602        });
13603        let (src_delta, scene_delta) = frontend
13604            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13605            .await
13606            .unwrap();
13607        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13608        assert_eq!(
13609            scene_delta.new_graph.objects.len(),
13610            4,
13611            "{:#?}",
13612            scene_delta.new_graph.objects
13613        );
13614
13615        ctx.close().await;
13616        mock_ctx.close().await;
13617    }
13618
13619    #[tokio::test(flavor = "multi_thread")]
13620    async fn test_lines_equal_length() {
13621        let initial_source = "\
13622sketch(on = XY) {
13623  line(start = [var 1, var 2], end = [var 3, var 4])
13624  line(start = [var 5, var 6], end = [var 7, var 8])
13625}
13626";
13627
13628        let program = Program::parse(initial_source).unwrap().0.unwrap();
13629
13630        let mut frontend = FrontendState::new();
13631
13632        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13633        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13634        let version = Version(0);
13635
13636        frontend.hack_set_program(&ctx, program).await.unwrap();
13637        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13638        let sketch_id = sketch_object.id;
13639        let sketch = expect_sketch(sketch_object);
13640        let line1_id = *sketch.segments.get(2).unwrap();
13641        let line2_id = *sketch.segments.get(5).unwrap();
13642
13643        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13644            lines: vec![line1_id, line2_id],
13645        });
13646        let (src_delta, scene_delta) = frontend
13647            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13648            .await
13649            .unwrap();
13650        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13651        assert_eq!(
13652            scene_delta.new_graph.objects.len(),
13653            9,
13654            "{:#?}",
13655            scene_delta.new_graph.objects
13656        );
13657
13658        ctx.close().await;
13659        mock_ctx.close().await;
13660    }
13661
13662    #[tokio::test(flavor = "multi_thread")]
13663    async fn test_add_constraint_multi_line_equal_length() {
13664        let initial_source = "\
13665sketch(on = XY) {
13666  line(start = [var 1, var 2], end = [var 3, var 4])
13667  line(start = [var 5, var 6], end = [var 7, var 8])
13668  line(start = [var 9, var 10], end = [var 11, var 12])
13669}
13670";
13671
13672        let program = Program::parse(initial_source).unwrap().0.unwrap();
13673
13674        let mut frontend = FrontendState::new();
13675        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13676        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13677        let version = Version(0);
13678
13679        frontend.hack_set_program(&ctx, program).await.unwrap();
13680        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13681        let sketch_id = sketch_object.id;
13682        let sketch = expect_sketch(sketch_object);
13683        let line1_id = *sketch.segments.get(2).unwrap();
13684        let line2_id = *sketch.segments.get(5).unwrap();
13685        let line3_id = *sketch.segments.get(8).unwrap();
13686
13687        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13688            lines: vec![line1_id, line2_id, line3_id],
13689        });
13690        let (src_delta, scene_delta) = frontend
13691            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13692            .await
13693            .unwrap();
13694        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13695        let constraints = scene_delta
13696            .new_graph
13697            .objects
13698            .iter()
13699            .filter_map(|obj| {
13700                let ObjectKind::Constraint { constraint } = &obj.kind else {
13701                    return None;
13702                };
13703                Some(constraint)
13704            })
13705            .collect::<Vec<_>>();
13706
13707        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13708        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13709            panic!("expected equal length constraint, got {:?}", constraints[0]);
13710        };
13711        assert_eq!(lines_equal_length.lines.len(), 3);
13712
13713        ctx.close().await;
13714        mock_ctx.close().await;
13715    }
13716
13717    #[tokio::test(flavor = "multi_thread")]
13718    async fn test_lines_parallel() {
13719        let initial_source = "\
13720sketch(on = XY) {
13721  line(start = [var 1, var 2], end = [var 3, var 4])
13722  line(start = [var 5, var 6], end = [var 7, var 8])
13723}
13724";
13725
13726        let program = Program::parse(initial_source).unwrap().0.unwrap();
13727
13728        let mut frontend = FrontendState::new();
13729
13730        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13731        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13732        let version = Version(0);
13733
13734        frontend.hack_set_program(&ctx, program).await.unwrap();
13735        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13736        let sketch_id = sketch_object.id;
13737        let sketch = expect_sketch(sketch_object);
13738        let line1_id = *sketch.segments.get(2).unwrap();
13739        let line2_id = *sketch.segments.get(5).unwrap();
13740
13741        let constraint = Constraint::Parallel(Parallel {
13742            lines: vec![line1_id, line2_id],
13743        });
13744        let (src_delta, scene_delta) = frontend
13745            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13746            .await
13747            .unwrap();
13748        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13749        assert_eq!(
13750            scene_delta.new_graph.objects.len(),
13751            9,
13752            "{:#?}",
13753            scene_delta.new_graph.objects
13754        );
13755
13756        ctx.close().await;
13757        mock_ctx.close().await;
13758    }
13759
13760    #[tokio::test(flavor = "multi_thread")]
13761    async fn test_lines_parallel_multiline() {
13762        let initial_source = "\
13763sketch(on = XY) {
13764  line(start = [var 1, var 2], end = [var 3, var 4])
13765  line(start = [var 5, var 6], end = [var 7, var 8])
13766  line(start = [var 9, var 10], end = [var 11, var 12])
13767}
13768";
13769
13770        let program = Program::parse(initial_source).unwrap().0.unwrap();
13771
13772        let mut frontend = FrontendState::new();
13773
13774        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13775        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13776        let version = Version(0);
13777
13778        frontend.hack_set_program(&ctx, program).await.unwrap();
13779        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13780        let sketch_id = sketch_object.id;
13781        let sketch = expect_sketch(sketch_object);
13782        let line1_id = *sketch.segments.get(2).unwrap();
13783        let line2_id = *sketch.segments.get(5).unwrap();
13784        let line3_id = *sketch.segments.get(8).unwrap();
13785
13786        let constraint = Constraint::Parallel(Parallel {
13787            lines: vec![line1_id, line2_id, line3_id],
13788        });
13789        let (src_delta, scene_delta) = frontend
13790            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13791            .await
13792            .unwrap();
13793        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13794
13795        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13796        let sketch = expect_sketch(sketch_object);
13797        assert_eq!(sketch.constraints.len(), 1);
13798
13799        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13800        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13801            panic!("Expected constraint object");
13802        };
13803        let Constraint::Parallel(parallel) = constraint else {
13804            panic!("Expected parallel constraint");
13805        };
13806        assert_eq!(parallel.lines.len(), 3);
13807
13808        ctx.close().await;
13809        mock_ctx.close().await;
13810    }
13811
13812    #[tokio::test(flavor = "multi_thread")]
13813    async fn test_lines_perpendicular() {
13814        let initial_source = "\
13815sketch(on = XY) {
13816  line(start = [var 1, var 2], end = [var 3, var 4])
13817  line(start = [var 5, var 6], end = [var 7, var 8])
13818}
13819";
13820
13821        let program = Program::parse(initial_source).unwrap().0.unwrap();
13822
13823        let mut frontend = FrontendState::new();
13824
13825        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13826        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13827        let version = Version(0);
13828
13829        frontend.hack_set_program(&ctx, program).await.unwrap();
13830        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13831        let sketch_id = sketch_object.id;
13832        let sketch = expect_sketch(sketch_object);
13833        let line1_id = *sketch.segments.get(2).unwrap();
13834        let line2_id = *sketch.segments.get(5).unwrap();
13835
13836        let constraint = Constraint::Perpendicular(Perpendicular {
13837            lines: vec![line1_id, line2_id],
13838        });
13839        let (src_delta, scene_delta) = frontend
13840            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13841            .await
13842            .unwrap();
13843        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13844        assert_eq!(
13845            scene_delta.new_graph.objects.len(),
13846            9,
13847            "{:#?}",
13848            scene_delta.new_graph.objects
13849        );
13850
13851        ctx.close().await;
13852        mock_ctx.close().await;
13853    }
13854
13855    #[tokio::test(flavor = "multi_thread")]
13856    async fn test_lines_angle() {
13857        let initial_source = "\
13858sketch(on = XY) {
13859  line(start = [var 1, var 2], end = [var 3, var 4])
13860  line(start = [var 5, var 6], end = [var 7, var 8])
13861}
13862";
13863
13864        let program = Program::parse(initial_source).unwrap().0.unwrap();
13865
13866        let mut frontend = FrontendState::new();
13867
13868        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13869        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13870        let version = Version(0);
13871
13872        frontend.hack_set_program(&ctx, program).await.unwrap();
13873        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13874        let sketch_id = sketch_object.id;
13875        let sketch = expect_sketch(sketch_object);
13876        let line1_id = *sketch.segments.get(2).unwrap();
13877        let line2_id = *sketch.segments.get(5).unwrap();
13878
13879        let constraint = Constraint::Angle(Angle {
13880            lines: vec![line1_id, line2_id],
13881            angle: Number {
13882                value: 30.0,
13883                units: NumericSuffix::Deg,
13884            },
13885            sector: None,
13886            inverse: None,
13887            label_position: None,
13888            source: Default::default(),
13889        });
13890        let (src_delta, scene_delta) = frontend
13891            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13892            .await
13893            .unwrap();
13894        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13895        assert_eq!(
13896            scene_delta.new_graph.objects.len(),
13897            9,
13898            "{:#?}",
13899            scene_delta.new_graph.objects
13900        );
13901
13902        ctx.close().await;
13903        mock_ctx.close().await;
13904    }
13905
13906    #[tokio::test(flavor = "multi_thread")]
13907    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13908        let initial_source = "\
13909sketch(on = XY) {
13910  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13911  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13912}
13913";
13914
13915        let program = Program::parse(initial_source).unwrap().0.unwrap();
13916
13917        let mut frontend = FrontendState::new();
13918
13919        let mock_ctx = ExecutorContext::new_mock(None).await;
13920        let version = Version(0);
13921
13922        frontend.program = program.clone();
13923        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13924        frontend.update_state_after_exec(outcome, true);
13925        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13926        let sketch_id = sketch_object.id;
13927        let sketch = expect_sketch(sketch_object);
13928        let line1_id = *sketch.segments.get(2).unwrap();
13929        let line2_id = *sketch.segments.get(5).unwrap();
13930
13931        let constraint = Constraint::Angle(Angle {
13932            lines: vec![line1_id, line2_id],
13933            angle: Number {
13934                value: 270.0,
13935                units: NumericSuffix::Deg,
13936            },
13937            sector: Some(1),
13938            inverse: Some(true),
13939            label_position: Some(Point2d {
13940                x: Number {
13941                    value: -0.73,
13942                    units: NumericSuffix::Mm,
13943                },
13944                y: Number {
13945                    value: 0.75,
13946                    units: NumericSuffix::Mm,
13947                },
13948            }),
13949            source: Default::default(),
13950        });
13951        let (src_delta, _) = frontend
13952            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13953            .await
13954            .unwrap();
13955        assert_eq!(
13956            src_delta.text.as_str(),
13957            "\
13958sketch(on = XY) {
13959  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13960  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13961  angleDimension(
13962    lines = [line1, line2],
13963    sector = 1,
13964    inverse = true,
13965    labelPosition = [-0.73mm, 0.75mm],
13966  ) == 270deg
13967}
13968"
13969        );
13970
13971        mock_ctx.close().await;
13972    }
13973
13974    #[tokio::test(flavor = "multi_thread")]
13975    async fn test_segments_tangent() {
13976        let initial_source = "\
13977sketch(on = XY) {
13978  line(start = [var 1, var 2], end = [var 3, var 4])
13979  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13980}
13981";
13982
13983        let program = Program::parse(initial_source).unwrap().0.unwrap();
13984
13985        let mut frontend = FrontendState::new();
13986
13987        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13988        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13989        let version = Version(0);
13990
13991        frontend.hack_set_program(&ctx, program).await.unwrap();
13992        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13993        let sketch_id = sketch_object.id;
13994        let sketch = expect_sketch(sketch_object);
13995        let line1_id = *sketch.segments.get(2).unwrap();
13996        let arc1_id = *sketch.segments.get(6).unwrap();
13997
13998        let constraint = Constraint::Tangent(Tangent {
13999            input: vec![line1_id, arc1_id],
14000        });
14001        let (src_delta, scene_delta) = frontend
14002            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14003            .await
14004            .unwrap();
14005        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14006        assert_eq!(
14007            scene_delta.new_graph.objects.len(),
14008            10,
14009            "{:#?}",
14010            scene_delta.new_graph.objects
14011        );
14012
14013        ctx.close().await;
14014        mock_ctx.close().await;
14015    }
14016
14017    #[tokio::test(flavor = "multi_thread")]
14018    async fn test_point_midpoint() {
14019        let initial_source = "\
14020sketch(on = XY) {
14021  point(at = [var 1, var 1])
14022  line(start = [var 0, var 0], end = [var 6, var 4])
14023}
14024";
14025
14026        let program = Program::parse(initial_source).unwrap().0.unwrap();
14027
14028        let mut frontend = FrontendState::new();
14029
14030        let ctx = ExecutorContext::new_mock(None).await;
14031        let version = Version(0);
14032
14033        frontend.program = program.clone();
14034        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14035        frontend.update_state_after_exec(outcome, true);
14036        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14037        let sketch_id = sketch_object.id;
14038        let sketch = expect_sketch(sketch_object);
14039        let point_id = *sketch.segments.first().unwrap();
14040        let line_id = *sketch.segments.get(3).unwrap();
14041
14042        let constraint = Constraint::Midpoint(Midpoint {
14043            point: ConstraintSegment::from(point_id),
14044            segment: line_id,
14045        });
14046        let (src_delta, scene_delta) = frontend
14047            .add_constraint(&ctx, version, sketch_id, constraint)
14048            .await
14049            .unwrap();
14050        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14051        assert_eq!(
14052            scene_delta.new_graph.objects.len(),
14053            7,
14054            "{:#?}",
14055            scene_delta.new_graph.objects
14056        );
14057
14058        ctx.close().await;
14059    }
14060
14061    #[tokio::test(flavor = "multi_thread")]
14062    async fn test_segments_symmetric() {
14063        let initial_source = "\
14064sketch(on = XY) {
14065  line(start = [var 0, var 0], end = [var 0, var 4])
14066  line(start = [var 4, var 0], end = [var 4, var 4])
14067  line(start = [var 2, var -1], end = [var 2, var 5])
14068}
14069";
14070
14071        let program = Program::parse(initial_source).unwrap().0.unwrap();
14072
14073        let mut frontend = FrontendState::new();
14074
14075        let ctx = ExecutorContext::new_mock(None).await;
14076        let version = Version(0);
14077
14078        frontend.program = program.clone();
14079        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14080        frontend.update_state_after_exec(outcome, true);
14081        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14082        let sketch_id = sketch_object.id;
14083        let sketch = expect_sketch(sketch_object);
14084        let line1_id = *sketch.segments.get(2).unwrap();
14085        let line2_id = *sketch.segments.get(5).unwrap();
14086        let axis_id = *sketch.segments.get(8).unwrap();
14087
14088        let constraint = Constraint::Symmetric(Symmetric {
14089            input: vec![line1_id, line2_id],
14090            axis: axis_id,
14091        });
14092        let (src_delta, scene_delta) = frontend
14093            .add_constraint(&ctx, version, sketch_id, constraint)
14094            .await
14095            .unwrap();
14096        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14097        assert_eq!(
14098            scene_delta.new_graph.objects.len(),
14099            12,
14100            "{:#?}",
14101            scene_delta.new_graph.objects
14102        );
14103
14104        ctx.close().await;
14105    }
14106
14107    #[tokio::test(flavor = "multi_thread")]
14108    async fn test_point_arc_midpoint() {
14109        let initial_source = "\
14110sketch(on = XY) {
14111  point(at = [var 6, var 3])
14112  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14113}
14114";
14115
14116        let program = Program::parse(initial_source).unwrap().0.unwrap();
14117
14118        let mut frontend = FrontendState::new();
14119
14120        let ctx = ExecutorContext::new_mock(None).await;
14121        let version = Version(0);
14122
14123        frontend.program = program.clone();
14124        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14125        frontend.update_state_after_exec(outcome, true);
14126        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14127        let sketch_id = sketch_object.id;
14128        let sketch = expect_sketch(sketch_object);
14129        let point_id = *sketch.segments.first().unwrap();
14130        let arc_id = *sketch.segments.get(4).unwrap();
14131
14132        let constraint = Constraint::Midpoint(Midpoint {
14133            point: ConstraintSegment::from(point_id),
14134            segment: arc_id,
14135        });
14136        let (src_delta, scene_delta) = frontend
14137            .add_constraint(&ctx, version, sketch_id, constraint)
14138            .await
14139            .unwrap();
14140        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14141        assert_eq!(
14142            scene_delta.new_graph.objects.len(),
14143            8,
14144            "{:#?}",
14145            scene_delta.new_graph.objects
14146        );
14147
14148        ctx.close().await;
14149    }
14150
14151    #[tokio::test(flavor = "multi_thread")]
14152    async fn test_origin_line_midpoint() {
14153        let initial_source = "\
14154sketch(on = XY) {
14155  line(start = [var 0, var 0], end = [var 6, var 4])
14156}
14157";
14158
14159        let program = Program::parse(initial_source).unwrap().0.unwrap();
14160
14161        let mut frontend = FrontendState::new();
14162
14163        let ctx = ExecutorContext::new_mock(None).await;
14164        let version = Version(0);
14165
14166        frontend.program = program.clone();
14167        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14168        frontend.update_state_after_exec(outcome, true);
14169        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14170        let sketch_id = sketch_object.id;
14171        let sketch = expect_sketch(sketch_object);
14172        let line_id = *sketch.segments.get(2).unwrap();
14173
14174        let constraint = Constraint::Midpoint(Midpoint {
14175            point: ConstraintSegment::ORIGIN,
14176            segment: line_id,
14177        });
14178        let (src_delta, scene_delta) = frontend
14179            .add_constraint(&ctx, version, sketch_id, constraint)
14180            .await
14181            .unwrap();
14182        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14183        assert_eq!(
14184            scene_delta.new_graph.objects.len(),
14185            6,
14186            "{:#?}",
14187            scene_delta.new_graph.objects
14188        );
14189
14190        ctx.close().await;
14191    }
14192
14193    #[tokio::test(flavor = "multi_thread")]
14194    async fn test_origin_arc_midpoint() {
14195        let initial_source = "\
14196sketch(on = XY) {
14197  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14198}
14199";
14200
14201        let program = Program::parse(initial_source).unwrap().0.unwrap();
14202
14203        let mut frontend = FrontendState::new();
14204
14205        let ctx = ExecutorContext::new_mock(None).await;
14206        let version = Version(0);
14207
14208        frontend.program = program.clone();
14209        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14210        frontend.update_state_after_exec(outcome, true);
14211        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14212        let sketch_id = sketch_object.id;
14213        let sketch = expect_sketch(sketch_object);
14214        let arc_id = *sketch.segments.get(3).unwrap();
14215
14216        let constraint = Constraint::Midpoint(Midpoint {
14217            point: ConstraintSegment::ORIGIN,
14218            segment: arc_id,
14219        });
14220        let (src_delta, scene_delta) = frontend
14221            .add_constraint(&ctx, version, sketch_id, constraint)
14222            .await
14223            .unwrap();
14224        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14225        assert_eq!(
14226            scene_delta.new_graph.objects.len(),
14227            7,
14228            "{:#?}",
14229            scene_delta.new_graph.objects
14230        );
14231
14232        ctx.close().await;
14233    }
14234
14235    #[tokio::test(flavor = "multi_thread")]
14236    async fn test_segments_symmetric_arcs() {
14237        let initial_source = "\
14238sketch(on = XY) {
14239  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14240  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14241  line(start = [var 0, var -10], end = [var 0, var 10])
14242}
14243";
14244
14245        let program = Program::parse(initial_source).unwrap().0.unwrap();
14246
14247        let mut frontend = FrontendState::new();
14248
14249        let ctx = ExecutorContext::new_mock(None).await;
14250        let version = Version(0);
14251
14252        frontend.program = program.clone();
14253        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14254        frontend.update_state_after_exec(outcome, true);
14255        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14256        let sketch_id = sketch_object.id;
14257        let sketch = expect_sketch(sketch_object);
14258        let arc1_id = *sketch.segments.get(3).unwrap();
14259        let arc2_id = *sketch.segments.get(7).unwrap();
14260        let axis_id = *sketch.segments.get(10).unwrap();
14261
14262        let constraint = Constraint::Symmetric(Symmetric {
14263            input: vec![arc1_id, arc2_id],
14264            axis: axis_id,
14265        });
14266        let (src_delta, scene_delta) = frontend
14267            .add_constraint(&ctx, version, sketch_id, constraint)
14268            .await
14269            .unwrap();
14270        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14271        assert_eq!(
14272            scene_delta.new_graph.objects.len(),
14273            14,
14274            "{:#?}",
14275            scene_delta.new_graph.objects
14276        );
14277
14278        ctx.close().await;
14279    }
14280
14281    #[tokio::test(flavor = "multi_thread")]
14282    async fn test_sketch_on_face_simple() {
14283        let initial_source = "\
14284len = 2mm
14285cube = startSketchOn(XY)
14286  |> startProfile(at = [0, 0])
14287  |> line(end = [len, 0], tag = $side)
14288  |> line(end = [0, len])
14289  |> line(end = [-len, 0])
14290  |> line(end = [0, -len])
14291  |> close()
14292  |> extrude(length = len)
14293
14294face = faceOf(cube, face = side)
14295";
14296
14297        let program = Program::parse(initial_source).unwrap().0.unwrap();
14298
14299        let mut frontend = FrontendState::new();
14300
14301        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14302        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14303        let version = Version(0);
14304
14305        frontend.hack_set_program(&ctx, program).await.unwrap();
14306        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14307        let face_id = face_object.id;
14308
14309        let sketch_args = SketchCtor {
14310            on: Plane::Object(face_id),
14311        };
14312        let (_src_delta, scene_delta, sketch_id) = frontend
14313            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14314            .await
14315            .unwrap();
14316        assert_eq!(sketch_id, ObjectId(2));
14317        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14318        let sketch_object = &scene_delta.new_graph.objects[2];
14319        assert_eq!(sketch_object.id, ObjectId(2));
14320        assert_eq!(
14321            sketch_object.kind,
14322            ObjectKind::Sketch(Sketch {
14323                args: SketchCtor {
14324                    on: Plane::Object(face_id),
14325                },
14326                plane: face_id,
14327                segments: vec![],
14328                constraints: vec![],
14329            })
14330        );
14331        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14332
14333        ctx.close().await;
14334        mock_ctx.close().await;
14335    }
14336
14337    #[tokio::test(flavor = "multi_thread")]
14338    async fn test_new_sketch_on_primitive_index_face() {
14339        let initial_source = "\
14340@settings(kclVersion = 2.0)
14341
14342sketch001 = sketch(on = XY) {
14343  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14344}
14345extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14346shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14347        let program = Program::parse(initial_source).unwrap().0.unwrap();
14348        let ctx = ExecutorContext::new_mock(None).await;
14349        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14350        let solid_id = match outcome.variables.get("shell001") {
14351            Some(KclValueView::Solid { value }) => value.id,
14352            value => panic!("expected shell001 to be a solid, got {value:?}"),
14353        };
14354        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14355
14356        let mut ast = program.ast;
14357        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14358        let face_expr = sketch_on_ast_expr(
14359            &mut ast,
14360            &scene_graph,
14361            &solid_references,
14362            &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14363        )
14364        .unwrap();
14365        let face_decl = ast::VariableDeclaration::new(
14366            ast::VariableDeclarator::new("face001", face_expr),
14367            ast::ItemVisibility::Default,
14368            ast::VariableKind::Const,
14369        );
14370        ast.body
14371            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14372                face_decl,
14373            ))));
14374        let face_source = source_from_ast(&ast);
14375        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14376        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14377
14378        let program = Program::parse(&new_source).unwrap().0.unwrap();
14379        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14380        ctx.close().await;
14381    }
14382
14383    #[tokio::test(flavor = "multi_thread")]
14384    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14385        let initial_source = "\
14386s = sketch(on = YZ) {
14387  line1 = line(start = [0, 0], end = [0, 1])
14388  line2 = line(start = [0, 1], end = [1, 1])
14389  line3 = line(start = [1, 1], end = [0, 0])
14390}
14391region001 = region(point = [0.1, 0.1], sketch = s)
14392extrude001 = extrude(region001, length = 5)
14393";
14394
14395        let program = Program::parse(initial_source).unwrap().0.unwrap();
14396
14397        let mut frontend = FrontendState::new();
14398        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14399        let version = Version(0);
14400
14401        frontend.hack_set_program(&ctx, program).await.unwrap();
14402        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14403
14404        let sketch_args = SketchCtor {
14405            on: Plane::Object(wall_object_id),
14406        };
14407        let (src_delta, _scene_delta, _sketch_id) = frontend
14408            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14409            .await
14410            .unwrap();
14411        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14412
14413        ctx.close().await;
14414    }
14415
14416    #[tokio::test(flavor = "multi_thread")]
14417    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14418        let initial_source = "\
14419sketch001 = sketch(on = YZ) {
14420  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14421  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14422  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14423  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14424  coincident([line1.end, line2.start])
14425  coincident([line2.end, line3.start])
14426  coincident([line3.end, line4.start])
14427  coincident([line4.end, line1.start])
14428  parallel([line2, line4])
14429  parallel([line3, line1])
14430  perpendicular([line1, line2])
14431  horizontal(line3)
14432  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14433}
14434region001 = region(point = [3.1, 3.74], sketch = sketch001)
14435extrude001 = extrude(region001, length = 5)
14436";
14437
14438        let program = Program::parse(initial_source).unwrap().0.unwrap();
14439
14440        let mut frontend = FrontendState::new();
14441        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14442        let version = Version(0);
14443
14444        frontend.hack_set_program(&ctx, program).await.unwrap();
14445        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14446
14447        let sketch_args = SketchCtor {
14448            on: Plane::Object(wall_object_id),
14449        };
14450        let (src_delta, _scene_delta, _sketch_id) = frontend
14451            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14452            .await
14453            .unwrap();
14454        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14455
14456        ctx.close().await;
14457    }
14458
14459    #[tokio::test(flavor = "multi_thread")]
14460    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14461        let initial_source = "\
14462@settings(kclVersion = 2.0)
14463
14464sketch001 = sketch(on = XY) {
14465  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14466  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14467}
14468hidden001 = hide(sketch001)
14469region001 = region(segments = [sketch001.circle2])
14470region002 = region(segments = [sketch001.circle1])
14471extrude001 = extrude([region001, region002], length = 5)
14472";
14473
14474        let program = Program::parse(initial_source).unwrap().0.unwrap();
14475        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14476        let version = Version(0);
14477
14478        for (solid_output_index, expected_face) in [
14479            (0, "faceOf(extrude001[0], face = END)"),
14480            (1, "faceOf(extrude001[1], face = END)"),
14481        ] {
14482            let mut frontend = FrontendState::new();
14483            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14484            let cap_object_id = find_cap_object_id_with_solid_output_index(
14485                &frontend.scene_graph,
14486                kcl_api::CapKind::End,
14487                solid_output_index,
14488            )
14489            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14490
14491            let sketch_args = SketchCtor {
14492                on: Plane::Object(cap_object_id),
14493            };
14494            let (src_delta, _scene_delta, _sketch_id) = frontend
14495                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14496                .await
14497                .unwrap();
14498
14499            assert!(
14500                src_delta.text.contains(expected_face),
14501                "expected `{expected_face}` in:\n{}",
14502                src_delta.text
14503            );
14504            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14505        }
14506
14507        ctx.close().await;
14508    }
14509
14510    #[tokio::test(flavor = "multi_thread")]
14511    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14512        let initial_source = "\
14513@settings(kclVersion = 2.0)
14514
14515sketch001 = sketch(on = XY) {
14516  rect1Line1 = line(start = [0, 0], end = [1, 0])
14517  rect1Line2 = line(start = [1, 0], end = [1, 1])
14518  rect1Line3 = line(start = [1, 1], end = [0, 1])
14519  rect1Line4 = line(start = [0, 1], end = [0, 0])
14520  rect2Line1 = line(start = [3, 0], end = [4, 0])
14521  rect2Line2 = line(start = [4, 0], end = [4, 1])
14522  rect2Line3 = line(start = [4, 1], end = [3, 1])
14523  rect2Line4 = line(start = [3, 1], end = [3, 0])
14524}
14525hidden001 = hide(sketch001)
14526region001 = region(segments = [
14527  sketch001.rect1Line4,
14528  sketch001.rect1Line1
14529])
14530region002 = region(segments = [
14531  sketch001.rect2Line4,
14532  sketch001.rect2Line1
14533])
14534extrude001 = extrude([region001, region002], length = 5)
14535";
14536
14537        let program = Program::parse(initial_source).unwrap().0.unwrap();
14538        let mut frontend = FrontendState::new();
14539        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14540        let version = Version(0);
14541
14542        frontend.hack_set_program(&ctx, program).await.unwrap();
14543        let region_call = "\
14544region(segments = [
14545  sketch001.rect1Line4,
14546  sketch001.rect1Line1
14547])";
14548        let region_call_start = initial_source.find(region_call).unwrap();
14549        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14550        let segment_call = "line(start = [0, 0], end = [1, 0])";
14551        let segment_call_start = initial_source.find(segment_call).unwrap();
14552        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14553        let wall_object_id = frontend
14554            .scene_graph
14555            .objects
14556            .iter()
14557            .find_map(|object| match &object.kind {
14558                ObjectKind::Wall(wall)
14559                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14560                        && wall.source.segment.range == segment_range =>
14561                {
14562                    Some(object.id)
14563                }
14564                _ => None,
14565            })
14566            .expect("expected a wall object for region001.tags.rect1Line1");
14567
14568        let sketch_args = SketchCtor {
14569            on: Plane::Object(wall_object_id),
14570        };
14571        let (src_delta, _scene_delta, _sketch_id) = frontend
14572            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14573            .await
14574            .unwrap();
14575
14576        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14577        assert!(
14578            src_delta.text.contains(expected_face),
14579            "expected `{expected_face}` in:\n{}",
14580            src_delta.text
14581        );
14582        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14583
14584        ctx.close().await;
14585    }
14586
14587    #[test]
14588    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14589        let source = "\
14590sketch001 = sketch(on = XY) {
14591  line(start = [0, 0], end = [1, 0])
14592}
14593part = subtract(boxSolid, tools = [cutSolid])
14594  |> appearance(color = \"#8f96a3\")
14595";
14596        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14597        let line_start = source.find("line").unwrap();
14598        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14599        let line_ref = SourceRef::Simple {
14600            range: [line_start, line_end, 0].into(),
14601            node_path: None,
14602        };
14603        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14604
14605        let subtract_start = source.find("subtract").unwrap();
14606        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14607        let subtract_ref = SourceRef::Simple {
14608            range: [subtract_start, subtract_end, 0].into(),
14609            node_path: None,
14610        };
14611        assert_eq!(
14612            variable_name_containing_source_ref(&ast, &subtract_ref),
14613            Some("part".to_owned())
14614        );
14615    }
14616
14617    #[tokio::test(flavor = "multi_thread")]
14618    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14619        let source = "\
14620boxSolid = startSketchOn(XY)
14621  |> startProfile(at = [0, 0])
14622  |> line(end = [4, 0], tag = $bottomEdge)
14623  |> line(end = [0, 4])
14624  |> line(end = [-4, 0])
14625  |> close()
14626  |> extrude(length = 10)
14627cutSolid = startSketchOn(XY)
14628  |> startProfile(at = [1, 1])
14629  |> line(end = [1, 0])
14630  |> line(end = [0, 1])
14631  |> line(end = [-1, 0])
14632  |> close()
14633  |> extrude(length = 10)
14634part = subtract(boxSolid, tools = [cutSolid])
14635  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14636";
14637        let program = Program::parse(source).unwrap().0.unwrap();
14638        let mut frontend = FrontendState::new();
14639        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14640        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14641            SetProgramOutcome::Success { .. } => {}
14642            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14643        }
14644
14645        let sweep_call_start = source.find("extrude").unwrap();
14646        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14647        let part_call_start = source.find("subtract").unwrap();
14648        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14649        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14650        let composite_range = [part_call_start, part_call_end, 0].into();
14651
14652        let cap_object = frontend
14653            .scene_graph
14654            .objects
14655            .iter()
14656            .find(|object| {
14657                matches!(
14658                    &object.kind,
14659                    ObjectKind::Cap(kcl_api::Cap {
14660                        kind: kcl_api::CapKind::End,
14661                        source,
14662                        ..
14663                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14664                )
14665            })
14666            .expect("expected end cap object to trace through subtract and original extrude");
14667
14668        let mut ast = frontend.program.ast.clone();
14669        let cap_expr = sketch_on_ast_expr(
14670            &mut ast,
14671            &frontend.scene_graph,
14672            &frontend.solid_references,
14673            &Plane::Object(cap_object.id),
14674        )
14675        .unwrap();
14676        let cap_face_decl = ast::VariableDeclaration::new(
14677            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14678            ast::ItemVisibility::Default,
14679            ast::VariableKind::Const,
14680        );
14681        ast.body
14682            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14683                cap_face_decl,
14684            ))));
14685        let generated_source = source_from_ast(&ast);
14686
14687        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14688        assert!(!generated_source.contains("faceOf(boxSolid"));
14689        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14690            panic!("expected faceOf call");
14691        };
14692        assert_eq!(call.callee.name.name, "faceOf");
14693        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14694            panic!("expected solid name");
14695        };
14696        assert_eq!(solid_name.name.name, "part");
14697        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14698            panic!("expected face name");
14699        };
14700        assert_eq!(face_name.name.name, "END");
14701
14702        ctx.close().await;
14703    }
14704
14705    #[tokio::test(flavor = "multi_thread")]
14706    async fn test_sketch_on_plane_incremental() {
14707        let initial_source = "\
14708len = 2mm
14709cube = startSketchOn(XY)
14710  |> startProfile(at = [0, 0])
14711  |> line(end = [len, 0], tag = $side)
14712  |> line(end = [0, len])
14713  |> line(end = [-len, 0])
14714  |> line(end = [0, -len])
14715  |> close()
14716  |> extrude(length = len)
14717
14718plane = planeOf(cube, face = side)
14719";
14720
14721        let program = Program::parse(initial_source).unwrap().0.unwrap();
14722
14723        let mut frontend = FrontendState::new();
14724
14725        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14726        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14727        let version = Version(0);
14728
14729        frontend.hack_set_program(&ctx, program).await.unwrap();
14730        // Find the last plane since the first plane is the XY plane.
14731        let plane_object = frontend
14732            .scene_graph
14733            .objects
14734            .iter()
14735            .rev()
14736            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14737            .unwrap();
14738        let plane_id = plane_object.id;
14739
14740        let sketch_args = SketchCtor {
14741            on: Plane::Object(plane_id),
14742        };
14743        let (src_delta, scene_delta, sketch_id) = frontend
14744            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14745            .await
14746            .unwrap();
14747        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14748        assert_eq!(sketch_id, ObjectId(2));
14749        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14750        let sketch_object = &scene_delta.new_graph.objects[2];
14751        assert_eq!(sketch_object.id, ObjectId(2));
14752        assert_eq!(
14753            sketch_object.kind,
14754            ObjectKind::Sketch(Sketch {
14755                args: SketchCtor {
14756                    on: Plane::Object(plane_id),
14757                },
14758                plane: plane_id,
14759                segments: vec![],
14760                constraints: vec![],
14761            })
14762        );
14763        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14764
14765        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14766        assert_eq!(plane_object.id, plane_id);
14767        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14768
14769        ctx.close().await;
14770        mock_ctx.close().await;
14771    }
14772
14773    #[tokio::test(flavor = "multi_thread")]
14774    async fn test_new_sketch_uses_unique_variable_name() {
14775        let initial_source = "\
14776sketch1 = sketch(on = XY) {
14777}
14778";
14779
14780        let program = Program::parse(initial_source).unwrap().0.unwrap();
14781
14782        let mut frontend = FrontendState::new();
14783        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14784        let version = Version(0);
14785
14786        frontend.hack_set_program(&ctx, program).await.unwrap();
14787
14788        let sketch_args = SketchCtor {
14789            on: Plane::Default(PlaneName::Yz),
14790        };
14791        let (src_delta, _, _) = frontend
14792            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14793            .await
14794            .unwrap();
14795
14796        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14797
14798        ctx.close().await;
14799    }
14800
14801    #[tokio::test(flavor = "multi_thread")]
14802    async fn test_new_sketch_twice_using_same_plane() {
14803        let initial_source = "\
14804sketch1 = sketch(on = XY) {
14805}
14806";
14807
14808        let program = Program::parse(initial_source).unwrap().0.unwrap();
14809
14810        let mut frontend = FrontendState::new();
14811        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14812        let version = Version(0);
14813
14814        frontend.hack_set_program(&ctx, program).await.unwrap();
14815
14816        let sketch_args = SketchCtor {
14817            on: Plane::Default(PlaneName::Xy),
14818        };
14819        let (src_delta, _, _) = frontend
14820            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14821            .await
14822            .unwrap();
14823
14824        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14825
14826        ctx.close().await;
14827    }
14828
14829    #[tokio::test(flavor = "multi_thread")]
14830    async fn test_sketch_mode_reuses_cached_on_expression() {
14831        let initial_source = "\
14832width = 2mm
14833sketch(on = offsetPlane(XY, offset = width)) {
14834  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14835  distance([line1.start, line1.end]) == width
14836}
14837";
14838        let program = Program::parse(initial_source).unwrap().0.unwrap();
14839
14840        let mut frontend = FrontendState::new();
14841        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14842        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14843        let version = Version(0);
14844        let project_id = ProjectId(0);
14845        let file_id = FileId(0);
14846
14847        frontend.hack_set_program(&ctx, program).await.unwrap();
14848        let initial_object_count = frontend.scene_graph.objects.len();
14849        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14850            .expect("Expected sketch object to exist")
14851            .id;
14852
14853        // Entering sketch mode should reuse cached `on` expression state
14854        // (offsetPlane result), not fail or create extra on-surface objects.
14855        let scene_delta = frontend
14856            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14857            .await
14858            .unwrap();
14859        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14860
14861        // A follow-up sketch-mode execution should keep the same stable object
14862        // graph shape as well.
14863        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14864        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14865
14866        ctx.close().await;
14867        mock_ctx.close().await;
14868    }
14869
14870    #[tokio::test(flavor = "multi_thread")]
14871    async fn test_edit_sketch_nested_in_pipe() {
14872        let source = r#"
14873profile = sketch(on = XY) {
14874  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14875}
14876  |> translate(x = 2mm)
14877"#;
14878        let program = Program::parse_no_errs(source).unwrap();
14879        let mut frontend = FrontendState::new();
14880        let mock_ctx = ExecutorContext::new_mock(None).await;
14881        let version = Version(0);
14882
14883        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14884        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14885            .expect("Expected piped sketch object")
14886            .id;
14887
14888        let scene_delta = frontend
14889            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14890            .await
14891            .unwrap();
14892        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14893        assert!(
14894            scene_delta
14895                .new_graph
14896                .objects
14897                .iter()
14898                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14899            "Expected the piped sketch's segments to be present in sketch mode"
14900        );
14901
14902        mock_ctx.cache.clear_mem_cache().await;
14903        mock_ctx.close().await;
14904    }
14905
14906    #[tokio::test(flavor = "multi_thread")]
14907    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14908        let source = r#"
14909useFirstProfile = true
14910
14911profile = if useFirstProfile {
14912  sketch(on = XY) {
14913    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14914  }
14915} else {
14916  sketch(on = XY) {
14917    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14918  }
14919}
14920"#;
14921        let program = Program::parse_no_errs(source).unwrap();
14922        let mut frontend = FrontendState::new();
14923        let mock_ctx = ExecutorContext::new_mock(None).await;
14924        let version = Version(0);
14925
14926        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14927        let sketch_object =
14928            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14929        let sketch_id = sketch_object.id;
14930        let sketch = expect_sketch(sketch_object);
14931        let line_end_id = *sketch
14932            .segments
14933            .get(1)
14934            .expect("Expected the active branch's line end point");
14935
14936        let scene_delta = frontend
14937            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14938            .await
14939            .unwrap();
14940        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14941
14942        let segments = vec![ExistingSegmentCtor {
14943            id: line_end_id,
14944            ctor: SegmentCtor::Point(PointCtor {
14945                position: Point2d {
14946                    x: Expr::Var(Number {
14947                        value: 30.0,
14948                        units: NumericSuffix::Mm,
14949                    }),
14950                    y: Expr::Var(Number {
14951                        value: 15.0,
14952                        units: NumericSuffix::Mm,
14953                    }),
14954                },
14955            }),
14956        }];
14957        let (source_delta, _) = frontend
14958            .edit_segments(&mock_ctx, version, sketch_id, segments)
14959            .await
14960            .unwrap();
14961        assert!(
14962            source_delta
14963                .text
14964                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14965            "Expected the active branch's dragged variables to be updated:\n{}",
14966            source_delta.text
14967        );
14968        assert!(
14969            source_delta
14970                .text
14971                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14972            "Expected the inactive branch to remain unchanged:\n{}",
14973            source_delta.text
14974        );
14975
14976        mock_ctx.cache.clear_mem_cache().await;
14977        mock_ctx.close().await;
14978    }
14979
14980    #[tokio::test(flavor = "multi_thread")]
14981    async fn test_multiple_sketch_blocks() {
14982        let initial_source = "\
14983// Cube that requires the engine.
14984width = 2
14985sketch001 = startSketchOn(XY)
14986profile001 = startProfile(sketch001, at = [0, 0])
14987  |> yLine(length = width, tag = $seg1)
14988  |> xLine(length = width)
14989  |> yLine(length = -width)
14990  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14991  |> close()
14992extrude001 = extrude(profile001, length = width)
14993
14994// Get a value that requires the engine.
14995x = segLen(seg1)
14996
14997// Triangle with side length 2*x.
14998sketch(on = XY) {
14999  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15000  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15001  coincident([line1.end, line2.start])
15002  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15003  coincident([line2.end, line3.start])
15004  coincident([line3.end, line1.start])
15005  equalLength([line3, line1])
15006  equalLength([line1, line2])
15007  distance([line1.start, line1.end]) == 2*x
15008}
15009
15010// Line segment with length x.
15011sketch2 = sketch(on = XY) {
15012  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15013  distance([line1.start, line1.end]) == x
15014}
15015";
15016
15017        let program = Program::parse(initial_source).unwrap().0.unwrap();
15018
15019        let mut frontend = FrontendState::new();
15020
15021        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15022        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15023        let version = Version(0);
15024        let project_id = ProjectId(0);
15025        let file_id = FileId(0);
15026
15027        frontend.hack_set_program(&ctx, program).await.unwrap();
15028        let sketch_objects = frontend
15029            .scene_graph
15030            .objects
15031            .iter()
15032            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15033            .collect::<Vec<_>>();
15034        let sketch1_id = sketch_objects.first().unwrap().id;
15035        let sketch2_id = sketch_objects.get(1).unwrap().id;
15036        // First point in sketch1.
15037        let point1_id = ObjectId(sketch1_id.0 + 1);
15038        // First point in sketch2.
15039        let point2_id = ObjectId(sketch2_id.0 + 1);
15040
15041        // Edit the first sketch. Objects before the sketch block should be
15042        // present from execution cache so that we can sketch on prior planes,
15043        // for example. Objects after the first sketch block should not be
15044        // present since those statements are skipped in sketch mode.
15045        //
15046        // - startSketchOn(XY) Plane 1
15047        // - sketch on=XY Plane 1
15048        // - Sketch block 16
15049        let scene_delta = frontend
15050            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15051            .await
15052            .unwrap();
15053        assert_eq!(
15054            scene_delta.new_graph.objects.len(),
15055            18,
15056            "{:#?}",
15057            scene_delta.new_graph.objects
15058        );
15059
15060        // Edit a point in the first sketch.
15061        let point_ctor = PointCtor {
15062            position: Point2d {
15063                x: Expr::Var(Number {
15064                    value: 1.0,
15065                    units: NumericSuffix::Mm,
15066                }),
15067                y: Expr::Var(Number {
15068                    value: 2.0,
15069                    units: NumericSuffix::Mm,
15070                }),
15071            },
15072        };
15073        let segments = vec![ExistingSegmentCtor {
15074            id: point1_id,
15075            ctor: SegmentCtor::Point(point_ctor),
15076        }];
15077        let (src_delta, _) = frontend
15078            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15079            .await
15080            .unwrap();
15081        // Only the first sketch block changes.
15082        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15083        let edited_sketch1_source = src_delta.text.clone();
15084
15085        // Execute mock to simulate drag end.
15086        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15087        assert_eq!(src_delta.text, edited_sketch1_source);
15088        // Exit sketch. Objects from the entire program should be present.
15089        //
15090        // - startSketchOn(XY) Plane 1
15091        // - sketch on=XY Plane 1
15092        // - Sketch block 16
15093        // - sketch on=XY cached
15094        // - Sketch block 5
15095        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15096        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15097
15098        // Edit the second sketch.
15099        //
15100        // - startSketchOn(XY) Plane 1
15101        // - sketch on=XY Plane 1
15102        // - Sketch block 16
15103        // - sketch on=XY cached
15104        // - Sketch block 5
15105        let scene_delta = frontend
15106            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15107            .await
15108            .unwrap();
15109        assert_eq!(
15110            scene_delta.new_graph.objects.len(),
15111            24,
15112            "{:#?}",
15113            scene_delta.new_graph.objects
15114        );
15115
15116        // Edit a point in the second sketch.
15117        let point_ctor = PointCtor {
15118            position: Point2d {
15119                x: Expr::Var(Number {
15120                    value: 3.0,
15121                    units: NumericSuffix::Mm,
15122                }),
15123                y: Expr::Var(Number {
15124                    value: 4.0,
15125                    units: NumericSuffix::Mm,
15126                }),
15127            },
15128        };
15129        let segments = vec![ExistingSegmentCtor {
15130            id: point2_id,
15131            ctor: SegmentCtor::Point(point_ctor),
15132        }];
15133        let (src_delta, _) = frontend
15134            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15135            .await
15136            .unwrap();
15137        // Only the second sketch block changes.
15138        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15139        let edited_sketch2_source = src_delta.text.clone();
15140
15141        // Execute mock to simulate drag end.
15142        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15143        assert_eq!(src_delta.text, edited_sketch2_source);
15144
15145        ctx.close().await;
15146        mock_ctx.close().await;
15147    }
15148
15149    #[tokio::test(flavor = "multi_thread")]
15150    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15151        let source = r#"sketch001 = sketch(on = XZ) {
15152  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15153}
15154sketch002 = sketch(on = XY) {
15155  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15156  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15157  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15158  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15159  coincident([line1.end, line2.start])
15160  coincident([line2.end, line3.start])
15161  coincident([line3.end, line4.start])
15162  coincident([line4.end, line1.start])
15163  parallel([line2, line4])
15164  parallel([line3, line1])
15165  perpendicular([line1, line2])
15166  horizontal(line3)
15167  coincident([line1.start, ORIGIN])
15168}
15169"#;
15170
15171        let program = Program::parse(source).unwrap().0.unwrap();
15172        let mut frontend = FrontendState::new();
15173        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15174        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15175        let version = Version(0);
15176        let project_id = ProjectId(0);
15177        let file_id = FileId(0);
15178
15179        frontend.hack_set_program(&ctx, program).await.unwrap();
15180        let sketch_objects = frontend
15181            .scene_graph
15182            .objects
15183            .iter()
15184            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15185            .collect::<Vec<_>>();
15186        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15187
15188        let sketch1_id = sketch_objects[0].id;
15189        let sketch2_id = sketch_objects[1].id;
15190
15191        frontend
15192            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15193            .await
15194            .unwrap();
15195        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15196
15197        let scene_delta = frontend
15198            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15199            .await
15200            .unwrap();
15201        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15202
15203        ctx.cache.clear_mem_cache().await;
15204        ctx.close().await;
15205        mock_ctx.close().await;
15206    }
15207
15208    // Regression tests: operations on source code with extra whitespace/newlines.
15209    // These test that NodePath-based lookups work correctly when source ranges
15210    // are shifted by extra whitespace that wouldn't be present after formatting.
15211
15212    #[tokio::test(flavor = "multi_thread")]
15213    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15214        // Extra newlines after @settings line - this shifts all source ranges.
15215        let initial_source = "@settings(defaultLengthUnit = mm)
15216
15217sketch001 = sketch(on = XY) {
15218  point(at = [1in, 2in])
15219}
15220";
15221
15222        let program = Program::parse(initial_source).unwrap().0.unwrap();
15223        let mut frontend = FrontendState::new();
15224
15225        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15226        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15227        let version = Version(0);
15228        let project_id = ProjectId(0);
15229        let file_id = FileId(0);
15230
15231        frontend.hack_set_program(&ctx, program).await.unwrap();
15232        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15233        let sketch_id = sketch_object.id;
15234
15235        // Edit sketch should succeed despite extra newlines.
15236        frontend
15237            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15238            .await
15239            .unwrap();
15240
15241        // Add a new point to the sketch.
15242        let point_ctor = PointCtor {
15243            position: Point2d {
15244                x: Expr::Number(Number {
15245                    value: 5.0,
15246                    units: NumericSuffix::Mm,
15247                }),
15248                y: Expr::Number(Number {
15249                    value: 6.0,
15250                    units: NumericSuffix::Mm,
15251                }),
15252            },
15253        };
15254        let segment = SegmentCtor::Point(point_ctor);
15255        let (src_delta, scene_delta) = frontend
15256            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15257            .await
15258            .unwrap();
15259        // After adding a point, the source should be reformatted with standard whitespace.
15260        assert!(
15261            src_delta.text.contains("point(at = [5mm, 6mm])"),
15262            "Expected new point in source, got: {}",
15263            src_delta.text
15264        );
15265        assert!(!scene_delta.new_objects.is_empty());
15266
15267        ctx.close().await;
15268        mock_ctx.close().await;
15269    }
15270
15271    #[tokio::test(flavor = "multi_thread")]
15272    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15273        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15274
15275        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15276        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15277
15278        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15279        assert!(
15280            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15281            "Expected experimental settings to be added to source"
15282        );
15283    }
15284
15285    #[tokio::test(flavor = "multi_thread")]
15286    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15287        // Extra newlines after @settings, with an empty sketch block.
15288        let initial_source = "@settings(defaultLengthUnit = mm)
15289
15290s = sketch(on = XY) {}
15291";
15292
15293        let program = Program::parse(initial_source).unwrap().0.unwrap();
15294        let mut frontend = FrontendState::new();
15295
15296        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15297        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15298        let version = Version(0);
15299
15300        frontend.hack_set_program(&ctx, program).await.unwrap();
15301        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15302        let sketch_id = sketch_object.id;
15303
15304        let line_ctor = LineCtor {
15305            start: Point2d {
15306                x: Expr::Number(Number {
15307                    value: 0.0,
15308                    units: NumericSuffix::Mm,
15309                }),
15310                y: Expr::Number(Number {
15311                    value: 0.0,
15312                    units: NumericSuffix::Mm,
15313                }),
15314            },
15315            end: Point2d {
15316                x: Expr::Number(Number {
15317                    value: 10.0,
15318                    units: NumericSuffix::Mm,
15319                }),
15320                y: Expr::Number(Number {
15321                    value: 10.0,
15322                    units: NumericSuffix::Mm,
15323                }),
15324            },
15325            construction: None,
15326        };
15327        let segment = SegmentCtor::Line(line_ctor);
15328        let (src_delta, scene_delta) = frontend
15329            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15330            .await
15331            .unwrap();
15332        assert!(
15333            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15334            "Expected line in source, got: {}",
15335            src_delta.text
15336        );
15337        // Line creates start point, end point, and line segment.
15338        assert_eq!(scene_delta.new_objects.len(), 3);
15339
15340        ctx.close().await;
15341        mock_ctx.close().await;
15342    }
15343
15344    #[tokio::test(flavor = "multi_thread")]
15345    async fn test_extra_newlines_between_operations_edit_line() {
15346        // Extra newlines between @settings and sketch, and inside the sketch block.
15347        let initial_source = "@settings(defaultLengthUnit = mm)
15348
15349sketch001 = sketch(on = XY) {
15350
15351  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15352
15353}
15354";
15355
15356        let program = Program::parse(initial_source).unwrap().0.unwrap();
15357        let mut frontend = FrontendState::new();
15358
15359        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15360        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15361        let version = Version(0);
15362        let project_id = ProjectId(0);
15363        let file_id = FileId(0);
15364
15365        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15366        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15367        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15368        let sketch_id = sketch_object.id;
15369        let sketch = expect_sketch(sketch_object);
15370
15371        // Extract segment IDs before edit_sketch borrows frontend mutably.
15372        let line_id = sketch
15373            .segments
15374            .iter()
15375            .copied()
15376            .find(|seg_id| {
15377                matches!(
15378                    &frontend.scene_graph.objects[seg_id.0].kind,
15379                    ObjectKind::Segment {
15380                        segment: Segment::Line(_)
15381                    }
15382                )
15383            })
15384            .expect("Expected a line segment in sketch");
15385
15386        // Enter sketch edit mode.
15387        frontend
15388            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15389            .await
15390            .unwrap();
15391
15392        // Edit the line.
15393        let line_ctor = LineCtor {
15394            start: Point2d {
15395                x: Expr::Var(Number {
15396                    value: 1.0,
15397                    units: NumericSuffix::Mm,
15398                }),
15399                y: Expr::Var(Number {
15400                    value: 2.0,
15401                    units: NumericSuffix::Mm,
15402                }),
15403            },
15404            end: Point2d {
15405                x: Expr::Var(Number {
15406                    value: 13.0,
15407                    units: NumericSuffix::Mm,
15408                }),
15409                y: Expr::Var(Number {
15410                    value: 14.0,
15411                    units: NumericSuffix::Mm,
15412                }),
15413            },
15414            construction: None,
15415        };
15416        let segments = vec![ExistingSegmentCtor {
15417            id: line_id,
15418            ctor: SegmentCtor::Line(line_ctor),
15419        }];
15420        let (src_delta, _scene_delta) = frontend
15421            .edit_segments(&mock_ctx, version, sketch_id, segments)
15422            .await
15423            .unwrap();
15424        assert!(
15425            src_delta
15426                .text
15427                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15428            "Expected edited line in source, got: {}",
15429            src_delta.text
15430        );
15431
15432        ctx.close().await;
15433        mock_ctx.close().await;
15434    }
15435
15436    #[tokio::test(flavor = "multi_thread")]
15437    async fn test_extra_newlines_delete_segment() {
15438        // Extra whitespace before and after the sketch block.
15439        let initial_source = "@settings(defaultLengthUnit = mm)
15440
15441sketch001 = sketch(on = XY) {
15442  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15443}
15444";
15445
15446        let program = Program::parse(initial_source).unwrap().0.unwrap();
15447        let mut frontend = FrontendState::new();
15448
15449        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15450        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15451        let version = Version(0);
15452
15453        frontend.hack_set_program(&ctx, program).await.unwrap();
15454        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15455        let sketch_id = sketch_object.id;
15456        let sketch = expect_sketch(sketch_object);
15457
15458        // The sketch should have 3 segments: start point, center point, and the circle.
15459        assert_eq!(sketch.segments.len(), 3);
15460        let circle_id = sketch.segments[2];
15461
15462        // Delete the circle despite extra newlines in original source.
15463        let (src_delta, scene_delta) = frontend
15464            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15465            .await
15466            .unwrap();
15467        assert!(
15468            src_delta.text.contains("sketch(on = XY) {"),
15469            "Expected sketch block in source, got: {}",
15470            src_delta.text
15471        );
15472        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15473        let new_sketch = expect_sketch(new_sketch_object);
15474        assert_eq!(new_sketch.segments.len(), 0);
15475
15476        ctx.close().await;
15477        mock_ctx.close().await;
15478    }
15479
15480    #[tokio::test(flavor = "multi_thread")]
15481    async fn test_unformatted_source_add_arc() {
15482        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15483        let initial_source = "@settings(defaultLengthUnit = mm)
15484
15485sketch001 = sketch(on = XY) {
15486}
15487";
15488
15489        let program = Program::parse(initial_source).unwrap().0.unwrap();
15490        let mut frontend = FrontendState::new();
15491
15492        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15493        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15494        let version = Version(0);
15495
15496        frontend.hack_set_program(&ctx, program).await.unwrap();
15497        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15498        let sketch_id = sketch_object.id;
15499
15500        let arc_ctor = ArcCtor {
15501            start: Point2d {
15502                x: Expr::Var(Number {
15503                    value: 5.0,
15504                    units: NumericSuffix::Mm,
15505                }),
15506                y: Expr::Var(Number {
15507                    value: 0.0,
15508                    units: NumericSuffix::Mm,
15509                }),
15510            },
15511            end: Point2d {
15512                x: Expr::Var(Number {
15513                    value: 0.0,
15514                    units: NumericSuffix::Mm,
15515                }),
15516                y: Expr::Var(Number {
15517                    value: 5.0,
15518                    units: NumericSuffix::Mm,
15519                }),
15520            },
15521            center: Point2d {
15522                x: Expr::Var(Number {
15523                    value: 0.0,
15524                    units: NumericSuffix::Mm,
15525                }),
15526                y: Expr::Var(Number {
15527                    value: 0.0,
15528                    units: NumericSuffix::Mm,
15529                }),
15530            },
15531            direction: None,
15532            construction: None,
15533        };
15534        let segment = SegmentCtor::Arc(arc_ctor);
15535        let (src_delta, scene_delta) = frontend
15536            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15537            .await
15538            .unwrap();
15539        assert!(
15540            src_delta
15541                .text
15542                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15543            "Expected arc in source, got: {}",
15544            src_delta.text
15545        );
15546        assert!(!scene_delta.new_objects.is_empty());
15547
15548        ctx.close().await;
15549        mock_ctx.close().await;
15550    }
15551
15552    #[tokio::test(flavor = "multi_thread")]
15553    async fn test_arc_direction_flows_to_source() {
15554        let initial_source = "@settings(defaultLengthUnit = mm)
15555
15556sketch001 = sketch(on = XY) {
15557}
15558";
15559
15560        let program = Program::parse(initial_source).unwrap().0.unwrap();
15561        let mut frontend = FrontendState::new();
15562
15563        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15564        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15565        let version = Version(0);
15566
15567        frontend.hack_set_program(&ctx, program).await.unwrap();
15568        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15569        let sketch_id = sketch_object.id;
15570
15571        let point = |x: f64, y: f64| Point2d {
15572            x: Expr::Var(Number {
15573                value: x,
15574                units: NumericSuffix::Mm,
15575            }),
15576            y: Expr::Var(Number {
15577                value: y,
15578                units: NumericSuffix::Mm,
15579            }),
15580        };
15581
15582        // Adding a clockwise arc writes its direction to the source.
15583        let arc_ctor = ArcCtor {
15584            start: point(5.0, 0.0),
15585            end: point(0.0, 5.0),
15586            center: point(0.0, 0.0),
15587            direction: Some(ArcDirection::Cw),
15588            construction: None,
15589        };
15590        let (src_delta, scene_delta) = frontend
15591            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15592            .await
15593            .unwrap();
15594        assert!(
15595            src_delta.text.contains("direction = CW"),
15596            "Expected direction = CW in source, got: {}",
15597            src_delta.text
15598        );
15599        // The new objects are the end points, the center, and then the arc.
15600        let arc_id = *scene_delta.new_objects.last().unwrap();
15601
15602        // Editing the arc's points preserves the clockwise direction. The
15603        // edited points keep the same distance to the center so that the
15604        // solver doesn't need to move anything.
15605        let edited_ctor = ArcCtor {
15606            start: point(0.0, -5.0),
15607            end: point(0.0, 5.0),
15608            center: point(0.0, 0.0),
15609            direction: Some(ArcDirection::Cw),
15610            construction: None,
15611        };
15612        let (src_delta, _scene_delta) = frontend
15613            .edit_segments(
15614                &mock_ctx,
15615                version,
15616                sketch_id,
15617                vec![ExistingSegmentCtor {
15618                    id: arc_id,
15619                    ctor: SegmentCtor::Arc(edited_ctor),
15620                }],
15621            )
15622            .await
15623            .unwrap();
15624        assert!(
15625            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15626            "Expected edited start point in source, got: {}",
15627            src_delta.text
15628        );
15629        assert!(
15630            src_delta.text.contains("direction = CW"),
15631            "Expected direction = CW to be preserved in source, got: {}",
15632            src_delta.text
15633        );
15634
15635        // Editing the arc back to counterclockwise removes the direction
15636        // argument since counterclockwise is the default.
15637        let edited_ctor = ArcCtor {
15638            start: point(0.0, -5.0),
15639            end: point(0.0, 5.0),
15640            center: point(0.0, 0.0),
15641            direction: Some(ArcDirection::Ccw),
15642            construction: None,
15643        };
15644        let (src_delta, _scene_delta) = frontend
15645            .edit_segments(
15646                &mock_ctx,
15647                version,
15648                sketch_id,
15649                vec![ExistingSegmentCtor {
15650                    id: arc_id,
15651                    ctor: SegmentCtor::Arc(edited_ctor),
15652                }],
15653            )
15654            .await
15655            .unwrap();
15656        assert!(
15657            !src_delta.text.contains("direction"),
15658            "Expected direction argument to be removed from source, got: {}",
15659            src_delta.text
15660        );
15661
15662        ctx.close().await;
15663        mock_ctx.close().await;
15664    }
15665
15666    #[tokio::test(flavor = "multi_thread")]
15667    async fn test_extra_newlines_add_circle() {
15668        // Extra blank lines between settings and sketch.
15669        let initial_source = "@settings(defaultLengthUnit = mm)
15670
15671sketch001 = sketch(on = XY) {
15672}
15673";
15674
15675        let program = Program::parse(initial_source).unwrap().0.unwrap();
15676        let mut frontend = FrontendState::new();
15677
15678        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15679        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15680        let version = Version(0);
15681
15682        frontend.hack_set_program(&ctx, program).await.unwrap();
15683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15684        let sketch_id = sketch_object.id;
15685
15686        let circle_ctor = CircleCtor {
15687            start: Point2d {
15688                x: Expr::Var(Number {
15689                    value: 5.0,
15690                    units: NumericSuffix::Mm,
15691                }),
15692                y: Expr::Var(Number {
15693                    value: 0.0,
15694                    units: NumericSuffix::Mm,
15695                }),
15696            },
15697            center: Point2d {
15698                x: Expr::Var(Number {
15699                    value: 0.0,
15700                    units: NumericSuffix::Mm,
15701                }),
15702                y: Expr::Var(Number {
15703                    value: 0.0,
15704                    units: NumericSuffix::Mm,
15705                }),
15706            },
15707            construction: None,
15708        };
15709        let segment = SegmentCtor::Circle(circle_ctor);
15710        let (src_delta, scene_delta) = frontend
15711            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15712            .await
15713            .unwrap();
15714        assert!(
15715            src_delta
15716                .text
15717                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15718            "Expected circle in source, got: {}",
15719            src_delta.text
15720        );
15721        assert!(!scene_delta.new_objects.is_empty());
15722
15723        ctx.close().await;
15724        mock_ctx.close().await;
15725    }
15726
15727    #[tokio::test(flavor = "multi_thread")]
15728    async fn test_extra_newlines_add_constraint() {
15729        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15730        let initial_source = "@settings(defaultLengthUnit = mm)
15731
15732sketch001 = sketch(on = XY) {
15733  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15734  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15735}
15736";
15737
15738        let program = Program::parse(initial_source).unwrap().0.unwrap();
15739        let mut frontend = FrontendState::new();
15740
15741        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15742        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15743        let version = Version(0);
15744        let project_id = ProjectId(0);
15745        let file_id = FileId(0);
15746
15747        frontend.hack_set_program(&ctx, program).await.unwrap();
15748        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15749        let sketch_id = sketch_object.id;
15750        let sketch = expect_sketch(sketch_object);
15751
15752        // Extract segment data before edit_sketch borrows frontend mutably.
15753        let line_ids: Vec<ObjectId> = sketch
15754            .segments
15755            .iter()
15756            .copied()
15757            .filter(|seg_id| {
15758                matches!(
15759                    &frontend.scene_graph.objects[seg_id.0].kind,
15760                    ObjectKind::Segment {
15761                        segment: Segment::Line(_)
15762                    }
15763                )
15764            })
15765            .collect();
15766        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15767
15768        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15769        let ObjectKind::Segment {
15770            segment: Segment::Line(line1_data),
15771        } = &line1.kind
15772        else {
15773            panic!("Expected line");
15774        };
15775        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15776        let ObjectKind::Segment {
15777            segment: Segment::Line(line2_data),
15778        } = &line2.kind
15779        else {
15780            panic!("Expected line");
15781        };
15782
15783        // Build constraint before entering sketch mode.
15784        let constraint = Constraint::Coincident(Coincident {
15785            segments: vec![line1_data.end.into(), line2_data.start.into()],
15786        });
15787
15788        // Enter sketch edit mode.
15789        frontend
15790            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15791            .await
15792            .unwrap();
15793        let (src_delta, _scene_delta) = frontend
15794            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15795            .await
15796            .unwrap();
15797        assert!(
15798            src_delta.text.contains("coincident("),
15799            "Expected coincident constraint in source, got: {}",
15800            src_delta.text
15801        );
15802
15803        ctx.close().await;
15804        mock_ctx.close().await;
15805    }
15806
15807    #[tokio::test(flavor = "multi_thread")]
15808    async fn test_extra_newlines_add_line_then_edit_line() {
15809        // Extra newlines after @settings - add a line, then edit it.
15810        let initial_source = "@settings(defaultLengthUnit = mm)
15811
15812sketch001 = sketch(on = XY) {
15813}
15814";
15815
15816        let program = Program::parse(initial_source).unwrap().0.unwrap();
15817        let mut frontend = FrontendState::new();
15818
15819        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15820        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15821        let version = Version(0);
15822
15823        frontend.hack_set_program(&ctx, program).await.unwrap();
15824        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15825        let sketch_id = sketch_object.id;
15826
15827        // Add a line.
15828        let line_ctor = LineCtor {
15829            start: Point2d {
15830                x: Expr::Number(Number {
15831                    value: 0.0,
15832                    units: NumericSuffix::Mm,
15833                }),
15834                y: Expr::Number(Number {
15835                    value: 0.0,
15836                    units: NumericSuffix::Mm,
15837                }),
15838            },
15839            end: Point2d {
15840                x: Expr::Number(Number {
15841                    value: 10.0,
15842                    units: NumericSuffix::Mm,
15843                }),
15844                y: Expr::Number(Number {
15845                    value: 10.0,
15846                    units: NumericSuffix::Mm,
15847                }),
15848            },
15849            construction: None,
15850        };
15851        let segment = SegmentCtor::Line(line_ctor);
15852        let (src_delta, scene_delta) = frontend
15853            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15854            .await
15855            .unwrap();
15856        assert!(
15857            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15858            "Expected line in source after add, got: {}",
15859            src_delta.text
15860        );
15861        // Line creates start point, end point, and line segment.
15862        let line_id = *scene_delta.new_objects.last().unwrap();
15863
15864        // Edit the line.
15865        let line_ctor = LineCtor {
15866            start: Point2d {
15867                x: Expr::Number(Number {
15868                    value: 1.0,
15869                    units: NumericSuffix::Mm,
15870                }),
15871                y: Expr::Number(Number {
15872                    value: 2.0,
15873                    units: NumericSuffix::Mm,
15874                }),
15875            },
15876            end: Point2d {
15877                x: Expr::Number(Number {
15878                    value: 13.0,
15879                    units: NumericSuffix::Mm,
15880                }),
15881                y: Expr::Number(Number {
15882                    value: 14.0,
15883                    units: NumericSuffix::Mm,
15884                }),
15885            },
15886            construction: None,
15887        };
15888        let segments = vec![ExistingSegmentCtor {
15889            id: line_id,
15890            ctor: SegmentCtor::Line(line_ctor),
15891        }];
15892        let (src_delta, scene_delta) = frontend
15893            .edit_segments(&mock_ctx, version, sketch_id, segments)
15894            .await
15895            .unwrap();
15896        assert!(
15897            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15898            "Expected edited line in source, got: {}",
15899            src_delta.text
15900        );
15901        assert_eq!(scene_delta.new_objects, vec![]);
15902
15903        ctx.close().await;
15904        mock_ctx.close().await;
15905    }
15906
15907    #[test]
15908    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15909        // A non-target sketch block appears before the target so that the
15910        // traversal enters and leaves it before reaching the target. The
15911        // generated name must come from the top-level scope, where foo1 is
15912        // taken, not the sketch's scope, where no foo names are taken. This
15913        // is a regression test: dfs_mut used to visit the sketch block twice,
15914        // pushing its scope twice but popping it once, leaving the sketch
15915        // scope on top of the defined-names stack for the rest of the
15916        // traversal.
15917        let code = "\
15918foo1 = 1
15919sk = sketch() {
15920  p = var 1.5
15921}
159227 + 8
15923";
15924        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15925        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15926            panic!("expected an expression statement");
15927        };
15928        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15929        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15930            &mut ast,
15931            &source_ref,
15932            AstMutateCommand::AddVariableDeclaration {
15933                prefix: "foo".to_owned(),
15934            },
15935        )
15936        .unwrap();
15937        let AstMutateCommandReturn::Name(name) = cmd_return else {
15938            panic!("expected a generated name");
15939        };
15940        assert_eq!(name, "foo2");
15941        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15942            panic!("expected the expression statement to become a variable declaration");
15943        };
15944        assert_eq!(decl.name(), "foo2");
15945    }
15946
15947    /// Get the function body of the variable declaration at `ast.body[index]`.
15948    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15949        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15950            panic!("expected a variable declaration");
15951        };
15952        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15953            panic!("expected a function expression");
15954        };
15955        &func.body
15956    }
15957
15958    /// Get the then-branch block of the if-expression initializing the
15959    /// variable declaration at `ast.body[index]`.
15960    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15961        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15962            panic!("expected a variable declaration");
15963        };
15964        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15965            panic!("expected an if expression");
15966        };
15967        &if_expr.then_val
15968    }
15969
15970    #[test]
15971    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15972        // The generated name must come from the function body's scope, where
15973        // thing1 is taken. Before dfs_mut visited function bodies as program
15974        // nodes, the top-level scope was used instead, generating thing1 and
15975        // colliding with the local.
15976        let code = "\
15977fn build() {
15978  thing1 = 1
15979  10 + 20
15980  return thing1
15981}
15982";
15983        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15984        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15985            panic!("expected an expression statement");
15986        };
15987        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15988        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15989            &mut ast,
15990            &source_ref,
15991            AstMutateCommand::AddVariableDeclaration {
15992                prefix: "thing".to_owned(),
15993            },
15994        )
15995        .unwrap();
15996        let AstMutateCommandReturn::Name(name) = cmd_return else {
15997            panic!("expected a generated name");
15998        };
15999        assert_eq!(name, "thing2");
16000        let body = &function_body_at(&ast, 0).body;
16001        assert_eq!(body.len(), 3);
16002        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16003            panic!("expected the expression statement to become a variable declaration");
16004        };
16005        assert_eq!(decl.name(), "thing2");
16006        // Siblings are untouched.
16007        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16008            panic!("expected a variable declaration");
16009        };
16010        assert_eq!(first.name(), "thing1");
16011        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16012    }
16013
16014    #[test]
16015    fn test_delete_node_in_function_body_preserves_leading_comment() {
16016        // Before the shared body traversal, MutateBodyItem::Delete was
16017        // silently dropped inside function bodies, so this reported success
16018        // without deleting anything.
16019        let code = "\
16020fn build() {
16021  a = 1
16022  // keep me
16023  b = 2
16024  return a
16025}
16026";
16027        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16028        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16029            panic!("expected a variable declaration");
16030        };
16031        assert_eq!(b_decl.name(), "b");
16032        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16033        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16034        let body = &function_body_at(&ast, 0).body;
16035        assert_eq!(body.len(), 2, "expected b to be deleted");
16036        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16037            panic!("expected a variable declaration");
16038        };
16039        assert_eq!(first.name(), "a");
16040        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16041            panic!("expected the return statement to remain");
16042        };
16043        assert!(
16044            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16045            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16046            body[1].get_comments()
16047        );
16048    }
16049
16050    #[test]
16051    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16052        // Locals in the function body are avoided: thing1 is taken, so the
16053        // generated name is thing2. But find_defined_names only sees the
16054        // block's body items, not the function's parameters, so the generated
16055        // name collides with the thing2 parameter. This pins the current
16056        // behavior.
16057        // TODO: Should function parameters be included in the scope used for
16058        // name generation?
16059        let code = "\
16060fn build(thing2) {
16061  thing1 = 1
16062  10 + 20
16063  return thing1 + thing2
16064}
16065";
16066        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16067        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16068            panic!("expected an expression statement");
16069        };
16070        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16071        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16072            &mut ast,
16073            &source_ref,
16074            AstMutateCommand::AddVariableDeclaration {
16075                prefix: "thing".to_owned(),
16076            },
16077        )
16078        .unwrap();
16079        let AstMutateCommandReturn::Name(name) = cmd_return else {
16080            panic!("expected a generated name");
16081        };
16082        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16083    }
16084
16085    #[test]
16086    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16087        let code = "\
16088x = 1
16089y = if x > 0 {
16090  q1 = 1
16091  foo(q1)
16092  q1
16093} else {
16094  2
16095}
16096";
16097        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16098        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16099            panic!("expected an expression statement");
16100        };
16101        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16102        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16103            &mut ast,
16104            &source_ref,
16105            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16106        )
16107        .unwrap();
16108        let AstMutateCommandReturn::Name(name) = cmd_return else {
16109            panic!("expected a generated name");
16110        };
16111        assert_eq!(name, "q2");
16112        let body = &then_block_at(&ast, 1).body;
16113        assert_eq!(body.len(), 3);
16114        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16115            panic!("expected the expression statement to become a variable declaration");
16116        };
16117        assert_eq!(decl.name(), "q2");
16118        // Siblings are untouched.
16119        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16120            panic!("expected a variable declaration");
16121        };
16122        assert_eq!(first.name(), "q1");
16123        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16124    }
16125
16126    #[test]
16127    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16128        // Before the shared body traversal, MutateBodyItem::Delete was
16129        // silently dropped inside if-expression branch blocks.
16130        let code = "\
16131y = if true {
16132  a = 1
16133  // keep me
16134  b = 2
16135  a + b
16136} else {
16137  2
16138}
16139";
16140        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16141        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16142            panic!("expected a variable declaration");
16143        };
16144        assert_eq!(b_decl.name(), "b");
16145        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16146        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16147        let body = &then_block_at(&ast, 0).body;
16148        assert_eq!(body.len(), 2, "expected b to be deleted");
16149        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16150            panic!("expected a variable declaration");
16151        };
16152        assert_eq!(first.name(), "a");
16153        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16154            panic!("expected the tail expression to remain");
16155        };
16156        assert!(
16157            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16158            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16159            body[1].get_comments()
16160        );
16161    }
16162}